1. 项目缘起与整体设计思路
1.1 为什么要在车规MCU上折腾XIP和FEE
芯驰E3118这颗芯片在车身域控、网关、区域控制器里出镜率越来越高。它基于ARM Cortex-R52内核,主频跑到600MHz,带锁步核,功能安全做到ASIL-D,片内Flash通常配置在4MB到8MB这个区间。做MCAL(Microcontroller Abstraction Layer)底层开发的人都知道,AUTOSAR架构下NVM(非易失性存储管理)这一块是最容易出幺蛾子的地方,而FEE(Flash EEPROM Emulation)又是NVM栈里最贴近硬件的那一层。
传统做法是把代码全部加载到RAM里跑,Flash只做存储。但E3118这类芯片的Flash访问速度其实不慢,配合预取和缓存,XIP(Execute In Place)模式下直接原地执行代码完全可行。XIP的好处很直接:省RAM。你不需要把几百KB的代码段搬到RAM里,省下来的RAM可以给FEE的缓存、诊断缓冲区、或者应用层的动态内存用。对于成本敏感的域控项目,RAM大小直接关系到芯片选型,省64KB RAM可能就意味着从高配版降到低配版,单颗芯片成本差出好几块钱。
FEE Demo的核心目标就一个:在XIP工程里把FEE跑通,实现数据的掉电保存和读取。听起来简单,但XIP和FEE放一起就有意思了——FEE要擦写Flash,而代码正在同一块Flash上执行,这中间的时序、中断、看门狗处理,每一步都是坑。
1.2 XIP工程和普通工程的本质区别
普通MCAL工程,链接脚本里代码段(.text)的加载地址(LMA)在Flash,运行地址(VMA)在RAM,启动时由启动代码把代码从Flash拷贝到RAM。XIP工程则不同,.text段的LMA和VMA都在Flash,启动时不需要拷贝,PC指针直接指向Flash地址取指执行。
这个区别带来的连锁反应很大。首先是启动时间缩短,省掉了代码拷贝阶段,对于要求快速启动的场景(比如倒车影像、紧急呼叫)很有价值。其次是RAM占用降低,但代价是Flash访问延迟。E3118的Flash控制器有预取缓冲和指令缓存,顺序执行时性能损失很小,但遇到分支跳转、中断向量跳转时,如果缓存没命中,会有几个周期的等待。实测下来,XIP模式下CoreMark跑分比RAM模式低大约8%到12%,这个损耗在大多数车身应用里完全可以接受。
FEE在XIP工程里的特殊之处在于:FEE的代码本身也在Flash里,当FEE执行擦除或写入操作时,Flash控制器会进入忙状态,此时如果CPU继续从同一Flash取指,就会stall。所以FEE的擦写函数必须搬到RAM里执行,这就是所谓的“Flash操作期间代码必须在RAM”的铁律。芯驰的MCAL包通常已经提供了FEE的RAM代码段,你需要做的就是在链接脚本里把这个段放到RAM,并且在调用FEE擦写API之前确保中断向量表也做了重映射(如果中断处理函数也在Flash里的话)。
1.3 Demo的整体架构设计
这个Demo的架构我拆成三层来看。最底层是芯驰E3118的Flash驱动(Fls),负责实际的擦除、写入、读取操作,操作对象是物理Flash扇区。中间层是FEE模块,它在Fls之上模拟EEPROM的行为,提供虚拟页(Virtual Page)的读写接口,内部管理Flash扇区的磨损均衡、数据校验、掉电恢复。最上层是NvM和应用层,NvM调用FEE的接口来读写Block。
Demo里我设计了两个Block:一个16字节的配置Block,存CAN通信参数和诊断ID;一个64字节的计数器Block,存里程、运行时间这类需要频繁更新的数据。FEE的扇区配置用了两个物理扇区做乒乓,每个扇区64KB,实际可用空间大约48KB(扣除头部和校验区)。磨损均衡策略采用“先写满一个扇区再切到另一个”的方式,配合垃圾回收。
注意:芯驰MCAL的FEE模块对扇区大小有最小要求,通常不能小于32KB。如果你选的Flash扇区是8KB,需要把多个物理扇区合并成一个FEE逻辑扇区来用。
2. 核心细节解析与实操要点
2.1 链接脚本的修改是XIP工程的第一道坎
XIP工程能不能跑起来,七成看链接脚本。芯驰MCAL包里的Demo工程通常自带一个默认的链接脚本,但那个脚本往往是给RAM调试模式用的。你要做XIP,必须改几个关键地方。
第一,.text段的VMA和LMA必须一致,都指向Flash的起始地址。E3118的Flash通常映射在0x80000000开始的空间,具体地址看芯片手册的Memory Map章节。第二,.ramcode段要单独定义,这个段用来放FEE的擦写函数和Flash驱动里那些不能在Flash执行时调用的函数。第三,中断向量表如果放在Flash里,FEE擦写期间发生中断就会导致CPU从忙状态的Flash取指,直接死锁。所以要么把向量表搬到RAM,要么在FEE擦写前关中断。
我用的链接脚本关键片段大概长这样:
MEMORY { FLASH (rx) : ORIGIN = 0x80000000, LENGTH = 4M RAM (rwx) : ORIGIN = 0x40000000, LENGTH = 512K } SECTIONS { .text : { *(.vectors) *(.text*) *(.rodata*) } > FLASH .ramcode : { __ramcode_start = .; *(.ramcode*) __ramcode_end = .; } > RAM AT > FLASH .data : { *(.data*) } > RAM AT > FLASH .bss : { *(.bss*) } > RAM }启动代码里需要把.ramcode段从Flash拷贝到RAM,这个拷贝动作在FEE初始化之前完成。芯驰的启动代码通常已经处理了.data段的拷贝,你只需要把.ramcode段加进去就行。
2.2 FEE模块的配置参数怎么定
FEE的配置参数直接决定了它的可靠性、寿命和性能。芯驰MCAL的FEE配置工具里,有几个参数必须仔细算。
扇区大小和数量:E3118的物理Flash扇区通常是8KB或16KB。FEE逻辑扇区建议用4个物理扇区合并,也就是32KB或64KB。用两个逻辑扇区做乒乓,总共需要64KB或128KB的Flash空间。如果你的项目Flash紧张,可以用一个逻辑扇区加一个备份区的方式,但磨损均衡效果会差一些。
Block大小和数量:每个Block的大小必须是8字节的整数倍,因为FEE内部用8字节对齐做校验。Block数量受限于逻辑扇区大小,计算公式是:可用Block数 = (逻辑扇区大小 - 头部开销) / (Block大小 + 元数据开销)。头部开销通常占2KB到4KB,元数据开销每个Block大约8到16字节。
写入粒度:FEE的写入粒度通常是8字节或16字节。如果你的Block大小是16字节,写入粒度是8字节,那一个Block需要两次写入操作。写入粒度越小,Flash利用率越高,但管理开销越大。
磨损均衡阈值:这个参数决定什么时候触发垃圾回收。设得太低,频繁擦写影响性能;设得太高,扇区寿命消耗快。一般建议设在70%到80%之间,也就是一个扇区用了70%到80%的空间后,开始把有效数据搬到另一个扇区。
实操心得:芯驰的FEE配置工具生成的代码里,扇区头部结构体是固定的,不要手动去改。如果你需要调整头部大小,改配置工具里的参数,让它重新生成代码。
2.3 Flash擦写期间的代码执行问题
这是XIP工程里最核心的技术点。当FEE调用Fls去擦除或写入Flash时,Flash控制器进入忙状态,此时任何从该Flash取指的操作都会导致CPU stall,直到擦写完成。擦除一个64KB扇区的时间通常在200ms到500ms之间,写入一个8字节的时间大约20us到50us。
解决方案是把所有在Flash忙期间需要执行的代码都放到RAM里。具体包括:FEE的擦写函数、Fls的底层操作函数、中断向量表(如果擦写期间不能关中断)、看门狗喂狗函数(如果擦写时间超过看门狗超时时间)。
芯驰MCAL包里的FEE模块通常已经用#pragma section或者__attribute__((section(".ramcode")))把关键函数标记到了RAM段。你需要做的是在链接脚本里正确放置这个段,并且在启动代码里完成拷贝。另外,擦写期间的中断处理要特别小心。如果中断向量表在Flash里,擦写期间发生中断,CPU会尝试从Flash取中断向量,直接卡死。所以要么在擦写前关中断,要么把向量表搬到RAM。
我一般建议在FEE擦写前关全局中断,擦写完成后恢复。虽然会增加中断延迟,但FEE擦写通常发生在下电前或者后台任务里,对实时性要求不高。如果必须开中断,那就把向量表搬到RAM,并且确保所有中断处理函数都在RAM里。
2.4 掉电保护与数据恢复机制
FEE的掉电保护靠的是“先写数据再写状态”的机制。每个Block在Flash里有一个状态字节,表示“有效”、“无效”、“正在写”。写入新数据时,先找一个空闲位置写入数据,然后把旧位置标记为无效,最后把新位置标记为有效。如果在任何一步掉电,上电后FEE扫描所有Block,根据状态字节判断哪些数据是完整的。
芯驰FEE模块支持两种掉电保护模式:一种是“双备份”,每个Block存两份,读的时候比较两份数据,取有效的那份;另一种是“日志式”,所有写入操作先写日志区,再写数据区。双备份模式简单可靠,但Flash利用率低;日志式模式复杂,但Flash利用率高。Demo里我用的是双备份模式,因为配置Block和计数器Block的数据量都不大,Flash空间够用。
注意:掉电保护的有效性依赖于Flash的写入原子性。E3118的Flash支持8字节原子写入,也就是说一次写入8字节要么全成功要么全失败,不会出现写了一半的情况。如果你的Block大小超过8字节,需要分多次写入,那就要在软件层面保证原子性。
3. 实操过程与核心环节实现
3.1 工程搭建与MCAL配置
第一步是拿到芯驰E3118的MCAL包和对应的配置工具。芯驰的MCAL配置工具通常是基于EB(Elektrobit)的tresos或者类似的框架,界面可能不太友好,但功能齐全。新建工程后,先配置MCU时钟、端口、Flash控制器这些基础模块。
Flash控制器的配置要注意几个参数:等待周期(Wait State)根据主频和Flash访问时间算,600MHz主频下通常需要4到6个等待周期;预取使能要打开,指令缓存和数据缓存都使能;擦写电压和时序参数用默认值就行,除非你的硬件设计有特殊要求。
FEE模块的配置在MCAL配置工具里单独有一个页面。你需要定义逻辑扇区的起始地址和大小、Block的ID和大小、写入粒度、磨损均衡阈值这些参数。配置完成后生成代码,工具会自动生成FEE_Cfg.c和FEE_Cfg.h这些配置文件。
Fls模块的配置相对简单,主要是定义物理扇区的起始地址、大小、擦写函数指针。芯驰的Fls驱动通常已经提供了擦写函数,你只需要把函数指针填到配置结构体里。
3.2 链接脚本与启动代码的适配
MCAL配置工具生成的工程通常自带一个链接脚本,但那个脚本是给RAM调试用的。你需要新建一个XIP版本的链接脚本,把.text段放到Flash,.ramcode段放到RAM。启动代码里要增加.ramcode段的拷贝逻辑。
启动代码的拷贝逻辑大概长这样:
extern uint32 __ramcode_start; extern uint32 __ramcode_end; extern uint32 __ramcode_load; void copy_ramcode(void) { uint32 *src = &__ramcode_load; uint32 *dst = &__ramcode_start; while (dst < &__ramcode_end) { *dst++ = *src++; } }这个函数要在main函数之前调用,通常在启动代码的SystemInit之后、main之前。芯驰的启动代码里有一个__main函数或者类似的入口,你可以在那里插入拷贝逻辑。
3.3 FEE初始化和读写测试
FEE初始化在EcuM_Init或者BswM_Init里调用,顺序是:先初始化Fls,再初始化FEE。FEE初始化会扫描所有逻辑扇区,检查头部信息,重建Block索引。如果检测到掉电恢复场景,会自动执行数据恢复。
初始化完成后,就可以调用FEE的读写接口了。写数据的流程是:调用Fee_Write(BlockId, DataPtr),FEE内部会找一个空闲位置写入数据,更新状态字节。读数据的流程是:调用Fee_Read(BlockId, Offset, DataPtr, Length),FEE根据Block索引找到最新有效数据,拷贝到用户缓冲区。
Demo里我写了三个测试用例:第一个是写入16字节配置数据,然后读出来比较;第二个是连续写入100次计数器数据,验证磨损均衡和垃圾回收;第三个是模拟掉电,在写入过程中复位芯片,上电后检查数据完整性。
测试结果:第一个用例一次通过。第二个用例在写入到第73次的时候触发了垃圾回收,回收过程耗时约180ms,期间看门狗差点超时,后来把看门狗超时时间从200ms改到500ms解决。第三个用例模拟了5次掉电,其中2次数据完整,3次数据回滚到上一次有效值,符合双备份模式的预期。
3.4 性能实测与优化
XIP模式下FEE的读写性能实测数据:读一个16字节Block耗时约2us,写一个16字节Block耗时约45us(包括状态字节更新),擦除一个64KB扇区耗时约320ms。垃圾回收一次(搬移约40KB有效数据)耗时约180ms。
优化方向有几个:第一,把FEE的写入操作放到后台任务里,避免阻塞应用层;第二,增大逻辑扇区大小,减少垃圾回收频率;第三,用日志式掉电保护模式替代双备份模式,提高Flash利用率。Demo里我用了第一种优化,把FEE写入放到一个10ms周期的任务里,每次写一个Block,应用层调用写接口时只是把数据放到队列里,不阻塞。
实操心得:FEE的垃圾回收时间跟有效数据量成正比。如果你的应用需要频繁写入,建议把逻辑扇区设大一点,比如128KB,这样垃圾回收频率低,但单次回收时间长。反过来,逻辑扇区小,回收频率高,但单次回收时间短。根据你的看门狗超时时间和任务周期来权衡。
4. 常见问题与排查技巧实录
4.1 FEE初始化失败怎么查
FEE初始化失败最常见的原因是Flash扇区头部数据损坏。芯驰的FEE模块在扇区头部存了魔数、版本号、校验和这些信息。如果头部数据不对,FEE会认为这个扇区不可用,初始化返回失败。
排查步骤:第一步,用调试器直接读Flash扇区起始地址的前64字节,看魔数对不对。芯驰的魔数通常是0x46454531("FEE1"的ASCII码)。第二步,如果魔数不对,检查链接脚本里FEE扇区的地址有没有跟代码段重叠。XIP工程里代码段从Flash起始地址开始放,FEE扇区如果也放在起始地址附近,就会被代码覆盖。第三步,如果魔数对但校验和不对,可能是Flash写入过程中掉电了,需要擦除整个扇区重新初始化。
我遇到过一次FEE初始化失败,查了半天发现是链接脚本里FEE扇区的地址跟中断向量表重叠了。中断向量表占了前1KB,FEE扇区从0x80000000开始,直接把向量表覆盖了。后来把FEE扇区挪到0x80100000之后解决。
4.2 写入数据后读出来不对
这个问题通常有三个原因:写入地址不对、数据长度不对、掉电保护机制干扰。
写入地址不对:FEE的Block ID和实际Flash地址的映射关系在配置工具里定义,如果配置工具里Block ID填错了,写入的数据会跑到别的Block去。检查方法是读配置工具生成的FEE_Cfg.c,看Block ID和扇区偏移的对应关系。
数据长度不对:FEE的写入长度必须是配置的Block大小的整数倍,或者至少是写入粒度的整数倍。如果写入长度不是整数倍,FEE会返回参数错误,但有些版本的MCAL会静默截断,导致读出来的数据不完整。
掉电保护机制干扰:双备份模式下,FEE读数据时会比较两份数据,取有效的那份。如果两份数据都有效但内容不同(比如写入过程中掉电),FEE会取时间戳更新的那份。如果你的测试用例没有考虑这种情况,可能会误判为数据错误。
4.3 垃圾回收期间看门狗复位
垃圾回收是FEE里最耗时的操作,搬移40KB数据需要180ms左右。如果看门狗超时时间设得比这个短,就会复位。解决方案有三个:第一,增大看门狗超时时间,比如从200ms改到500ms;第二,在垃圾回收期间喂狗,但要注意喂狗函数必须在RAM里执行;第三,把垃圾回收拆分成多次小操作,每次搬移4KB,分10次完成,每次之间喂狗。
我一般用第三种方案,虽然代码复杂一点,但最稳妥。芯驰的FEE模块支持配置垃圾回收的步长,你可以在配置工具里把回收步长从“整个扇区”改成“4KB”,这样每次回收只搬移4KB,耗时约18ms,看门狗完全来得及。
4.4 XIP模式下代码跑飞
XIP模式下代码跑飞的原因通常是Flash访问冲突。当FEE擦写Flash时,如果CPU从同一Flash取指,就会stall。如果stall时间过长,看门狗复位,或者CPU直接跑飞。
排查方法:第一步,确认所有在Flash忙期间执行的代码都在RAM里。用调试器查看PC指针,如果PC指向Flash地址范围,说明代码没搬到RAM。第二步,检查链接脚本里.ramcode段的地址范围,确保它跟Flash地址不重叠。第三步,检查启动代码里.ramcode段的拷贝逻辑,确保拷贝长度正确。
我遇到过一次代码跑飞,查到最后发现是中断向量表没搬到RAM。FEE擦写期间发生了一个定时器中断,CPU从Flash取中断向量,Flash忙导致stall,看门狗复位。后来把向量表搬到RAM,并且在FEE擦写前关中断,问题解决。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| FEE初始化失败 | 扇区头部损坏 | 读Flash前64字节看魔数 | 擦除扇区重新初始化 |
| 写入后读出不对 | Block ID映射错误 | 查FEE_Cfg.c配置 | 修正Block ID |
| 垃圾回收看门狗复位 | 回收时间过长 | 测量回收耗时 | 增大看门狗超时或拆分回收 |
| XIP代码跑飞 | 代码未搬到RAM | 查PC指针地址 | 修正链接脚本和启动代码 |
| 掉电后数据丢失 | 掉电保护未生效 | 检查状态字节 | 启用双备份或日志模式 |
| 写入返回忙 | 前一次操作未完成 | 查FEE状态寄存器 | 等待或增加重试机制 |
最后再分享一个小技巧:芯驰的FEE模块有一个调试接口,可以打印内部状态机的状态和Block索引表。在调试阶段打开这个接口,能省很多排查时间。正式发布时记得关掉,否则会影响性能。