Memory Stack 开发总结
前言
截止目前,已经完成了 FlashDriver、Fls、Fee、MemIf、NvM 的开发。开发的 Memory Stack 只是借鉴了 AUTOSAR Memory Stack 架构,并非完全相同,为了兼容旧版软件,也有部分不符合 AUTOSAR 规范的地方。为了解决一些特殊需求,也有一些创新之处。
参考资料
- AUTOSAR 官方资料
- https://www.embeddedtutor.com/search/label/Autosar
设计方式
FlashDriver
Flash驱动层,这一层AUTOSAR中是没有的,我的理解是,AUTOSAR的Fls是Fee代码组件是由芯片厂家提供,那么mcu厂家提供的代码不会兼容其他厂家的外部Flash。代码我只看了云途配置工具生成的Fls代码和NXP官网上的一些芯片的BSW库,并没有见到过有外部Flash厂家提供的Fls代码。为了解决这个问题,我对Fls做了抽象,将不同Flash相关的代码和Fls分开,故有FlashDriver层。不同Flash有共性同时也有特性,为保留特性,借鉴了Linux中对各种外设共有属性和私有属性的思想,这个我是在看《嵌入式C语言自我修养——从芯片、编译器到操作系统》里学的。
Flash驱动层对上层Fls通过一个成员全是函数指针的结构体提供接口,如下:
1 | typedef struct { |
此结构体在每个FlashDriver中有定义,并且是对外的全局变量。用户在配置Fls时,可以配置此结构体指针,这样Fls就可以使用不同的Flash驱动了。
保留特性
每种Flash驱动对应一份FlashDriver代码,目前已完成两种FlashDriver开发,一个是mcu内部Flash,另一个是spi通信的外部Flash。每个Flash共有的属性有起始地址、扇区大小(最小擦除量)、扇区个数、页大小(最大写入量),特性有不同的通信方式。如外部Flash SPI通信,需要配置其SPI ID,这是在有MCAL(mcu硬件抽象层)的基础上,如果没有MCAL FlashDriver这一层需要把SPI配置都用用户配置的方式实现,方便封库。
为保留特性,FlashDriver每个接口都由上层传递了FlsPrivateConfigPtr
,接口定义里这是个void指针,在每个FlashDriver的接口实现里将其转成了自己特性配置的结构体指针类型。特性配置结构体类型声明在每个FlashDriver的头文件中,由用户在Fls用户代码中定义配置,再配置进Fls层,上层Fls只是做了传递这个配置给FlashDriver,Fls无法解析这个配置。
像mcu内部Flash好像并不需要什么特性配置,读写方式都是定死的。不需要FlsPrivateConfigPtr
可以在配置Fls时将这个指针配成NULL。
阻塞在哪
阻塞在哪里也是个问题,在开发Fls时才想到,一开始我都是阻塞在FlashDriver,因为实现方便,写入和擦除操作可以直接查Flash状态阻塞到完成。后来Fls想做同时支持同步也支持异步的方式,还有需求是Fls超时结束任务,这就需要有Fls去控制是否还要继续阻塞。故FlashDriver中就不阻塞,新增一个读状态接口Flash_GetStatus
,阻塞在Fls层。
但阻塞在FlashDriver这个配置也保留了,可以通过宏选择是否阻塞在驱动。为什么留?写入、擦除出现问题时,改这个成阻塞也不管超时,可以判断下是不是异步导致的问题,调试也能更好的定位问题(阻塞在有问题的地方,可以分析函数调用栈)。 目前Flash底层驱动可以提供阻塞或非阻塞的接口让Fls调用,甚至可以通过私有配置实现部分地址使用阻塞,部分地址使用非阻塞的方式。
其余问题
其余问题是云途这个Flash驱动的问题,如果不严格按例程操作寄存器写,只按手册命令方式写的话坑比较多,可能开发的时候能用,测试也没问题,但集成到整个项目工程里时有玄学问题。
Fls
按AUTOSAR手册,Fls主要是Flash驱动的功能,对上层提供各种异步读写擦的功能,支持查询任务状态、运行结果。Fls可以将不同地址统一成一个从零开始的线性地址,这个有什么用?假如我想使用两个不连续的Flash扇区,Fls做个地址映射到这两个扇区,对上层来说地址是连续的,上层无需管Flash物理地址,操作的都是逻辑地址,方便上层的使用。
Fls把FlashDriver抽象出来,增加一种flash需编写一份Flash底层驱动,无需修改Fls代码,可在Flash底层驱动里实现不同Flash的私有配置。Fls会传递正确的物理地址和长度等参数给FlashDriver,因此FlashDriver可不用重复检查参数。Fls可以配置多个实例,支持多个实例用同一份FlashDriver,也支持同时有多种不同的Fls实例,他们共用一份Fls代码(增加复用性)。
异步
为什么使用异步?同步阻塞时间过大,影响其他任务运行,异步可以将一个大任务打散成多个小任务多次执行。Fls要实现异步方式调用,任务会在Fls_main里执行,用户可配每次Fls_main执行任务最大的读写量,防止一次main阻塞时间过长。
异步除了在实现的时候会比同步复杂些,用户在调用的时候也会比同步复杂,有os支持下还好,当任务异步调用Fls后可以挂起,定时查询状态或回调的方式,当Fls任务结束后继续执行原任务,假如没有os支持的话,没有挂起接口,用户每次异步调完Fls后需要记录下当前运行的位置,周期调度下次继续执行。异步能让io(flash)操作阻塞时mcu去干别的事,更好的利用处理器性能,但也增加了查询、任务切换等开销。Fls异步写入时,将写入任务拆成一个个异步的页写入任务,执行后内部挂起,等下次执行查状态。像云途的Flash页大小就8字节,岂不是写256字节要分成30多次子任务,这进出函数的开销和利用阻塞的时间,异步到底有没有优化性能呢?
擦除任务异步挺有意义的每次阻塞的时间长,也不用把任务打的很散增大函数出入的开销。GD的外部Flash页有256字节,每次写入阻塞时间较长,这种也适用异步写,车规芯片的SPI速度较低最大4M,需要SPI中断或DMA的支持。
同步
为何保留同步?通过一个参数来决定是同步执行还是异步执行,同步的好处使用户方便使用。
同步如何实现?其实异步实现了,同步自然就实现了。同步就是一直调用Fls内部的main直到任务执行完毕。
配置
Fls需要配置哪些参数?凡是在Fls配置的都是Flash的共有属性,配置较多:
1 | typedef struct { |
由于要封模块,并且要做成可重入,所以需要由用户在配置代码中定义每个实例的管理ram,并配置到config常量中。
线性地址
从零开始的线性地址如何实现?看了上面的Fls_SectorType
配置可以想到,Flash的地址映射关系就是映射到的“扇区列表配置”中。当时有个问题,假如有多个Flash,是不是映射成一个从零开始的地址?这样的话上层调用无需体现实例号的,因为根据地址映射关系就能找到要操作的Flash。后来想想还是不行,需要每个Flash的地址都分开从零开始,因为不同Flash的扇区大小等不同,上层不好只通过地址分辨是哪个Flash然后操作。
多实例同时运行
这个特性也是需要支持的,不能让一个Flash阻塞的时候另一个Flash无法使用。如何实现这个特性呢?为每个Fls实例开一个Fls_JobInfoType
管理实例的运行状态,每次Fls_main都把不同实例都执行一遍。这里有个可改善点,每次Fls_main只执行一个实例,让Fls_main的周期短一些,可以把多个Fls实例执行的阻塞时间打散一些,整个系统的最大阻塞时间会缩短。
对外接口
对外接口大致遵循AUTOSAR规范,增加了实例号和是否同步的参数。
1 | void Fls_Init(const Fls_ConfigType *ConfigPtr, boolean IsSync); |
Fee
AUTOSAR NvM中的Fee(Flash EEPROM Emulation)主要做Flash模拟EEPROM的功能。使用EEPROM,软件可以在任意字节读写,也无需管擦除的事情。使用Flash需要考虑每次写入是否对齐、擦除一整个扇区的时候会不会有其他的数据被擦除,总不能每次写就擦除一整个扇区,一个扇区里就放一点点要存的内容吧。不管是Flash还是EEP通常都有磨损均衡的算法,提高Flash的寿命与利用率。Fee就是干这个事情的,并且要留出和Ea(EEPROM Abstraction Layer)一样的接口供上层MemIf(Memory Abstraction Interface)调用,在NvM操作MemIf,无需关心是Fee还是Ea。这也体现了AUTOSAR Memory Stack的高扇入低扇出思想,对外统一接口,内部可以操作不同的非易失存储器。
Fee是我在实现AUTOSAR Memory Stack中,最复杂的一个模块,先介绍一个基础概念:在内存协议栈中,每个要读写的数据为一个Block数据块,NvM会调用MemIf读写Block,MemIf会调用相应的子设备如Fee。
均衡算法与Cluster概念
均衡算法参考了AUTOSAR的多Cluster与多Cluster Group,也加入了自己的均衡磨损管理算法,最终可以实现安全可靠的Block存储。
在Fee中,一个Flash被划分成1个或多个Cluster Group,每个Cluster Group中可以管理不同的Block;一个Cluster Group中有2个或多个Cluster,用来均衡磨损,每次写入Block都会在所属的Cluster Group中的一个活跃的Cluster上写入,当所有Cluster被写满时,最老的一个Cluster会被擦除,然后此Cluster待写入。
每个Cluster Group都可以由用户配置,并且都有自己运行时的一份变量空间,一下是Cluster Group的配置与变量:
1 | typedef struct { |
每个Cluster有它的管理数据结构,Cluster Header中包含了所属哪个Cluster Group、Cluster Id、Cluster Size、擦除次数、状态等信息。初始化会检查这些Cluster头,避免Flash中的Cluster配置与代码不一致造成的数据问题,还可以通过擦除次数看Flash的使用次数,评估其寿命,异常时做云平台上报。
查找最新的Block
每次上电初始化要做的事就是从Flash中读取Block,那存在Cluster中的哪个Block是最新的呢?首先要知道一个Cluster Group中哪个Cluster是最新的。Cluster Header中会有Cluster的状态,一般情况下最新的Cluster有个活跃的状态,便可知最老的那个Cluster,从最老的Cluster上遍历查找每个Block,记录每个Block最新地址,这么一轮遍历结束就可以知道各个Block最新的数据存在哪个位置啦。
内部接口:static Std_ReturnType Fee_ScanBlock(uint8 FeeInstanceId, uint8 ClusterGroup, uint16 ScanStartClusterId)
索引是从指定Cluster搜索有效的block,取最新的block信息保存至FeeBlockInfo。考虑Cluster中会有损坏的Block和之前写Block中断电的情况,Scan需要能跳过坏Block,按以下流程scan每个有效的Cluster:
- 每个Cluster开始索引的地址为ClusterHeader后开始存放Block的地址。
- 开始检查对应地址的Block状态。
- 为空,跳出此Cluster的索引。
- DataValid有效,Block正常,将Block地址存储在BlockInfo(hash表)中,根据此Block长度跳地址,继续索引(步骤2)。
- NumLenValid(BlockInfo)有效,Block数据段无效,数据段无法保证全为空, 跳过BlockHeader+BlockLen长度继续索引。
- NumLen和Data都无效,BlockHeader不为空正常情况下数据区还没被写,跳过一个BlockHeader长度继续索引。
每个Block在Fee中都有其对应的配置与RAM空间,如下:
1 | typedef struct { |
每个Block也像Cluster一样有Header,在Block Header中记录BlockId、BlockSize、各种标志位信息。可以通过这些标志位,判断当时存的时候有没有异常下电,Block数据是否完整可信,在Fee中不是通过CRC的方式判断数据是否可信,因为在初始化遍历读取Block的时候,给每个Block做CRC如果Flash很大那需要很长一段时间。
交换Cluster
内部接口:static Std_ReturnType Fee_SwapCluster(uint8 FeeInstanceId, uint8 ClusterGroup, uint16 FromClusterId, uint16 ToClusterId)
将旧Cluster上的有效Block搬到新Cluster。
- 遍历所有的BlockInfo,如果Block的ClusterGroup匹配,且有效数据在老的Cluster上,就将整个Block读取到函数栈空间,然后写入新Cluster的地址,在SWAPPING的时候无需考虑写BlockHeader标志位的顺序,因为SWAPPING时掉电,下次初始化时会擦除此Cluster重新SWAP。
详细描述:当Cluster总数-1的Cluster被写完的情况下就需要触发Cluster交换任务了,目的是将最老Cluster上有效的Block(没有更新的数据)转移到那块空白的Cluster上,我称这个操作叫换页。换页时会给Cluster写上换页的标志位,换页过程中异常下电下次能识别到触发重新换页任务。上面说过每个Block都有ram空间,管理这其最新Block的地址(在哪个Cluster上和偏移地址),因此很容易知道最老的Cluster有哪些有效Block和其地址,可以简单的使用memcpy将其复制到最新的Cluster上,因为Block Header不用发生更改,并且写入Block时无需防止异常下电。换页结束后需改变Cluster的状态,将原先最新的那个从活跃设置为满状态,将新的那个Cluster从空设为活跃状态。
维护任务
内部接口:static Std_ReturnType Fee_MaintainJob(uint8 FeeInstanceId)
维护任务,需要检查是否需要执行交换和擦除任务。考虑减少运行时阻塞,正常情况维护中擦除Cluster采用异步非阻塞的方式。正常情况维护操作流程如下:(非正常情况处理较复杂不写)
- 当前操作的Cluster状态为ACTIVE,检查下个Cluster状态是否为VERIFIED,若不是,异步擦除下个Cluster。
- 当前操作的Cluster状态为ACTIVE,下个Cluster状态为VERIFIED,检查当前Cluster空闲空间是否充足,若不足,将当前Cluster写FULL标志位,2个Cluster和2个以上Cluster写入标志位顺序有差异。
- 当前操作的Cluster状态为FULL,执行SWAP任务,将最老的Cluster上有效的Block搬到新的Cluster上,SWAP成功后将当前操作的Cluster改为新的Cluster,新Cluster状态设为ACTIVE。
Cluster 状态转移图如下:
初始化
对外接口:void Fee_Init(const Fee_ConfigType* ConfigPtr, boolean IsSync)
内部初始化Job接口:static Std_ReturnType Fee_InitJob(uint8 FeeInstanceId)
初始化Job中需要处理各种不同的Cluster状态,查找ACTIVE状态的Cluster和最老的Cluster,从最老的Cluster开始索引Block。
考虑在换页时断电和Cluster损坏的可能性,初始化处理Cluster状态有如下几种情况:(每种情况处理方式不描述了)
- 只有1个ACTIVE状态的cluster。
- 没有ACTIVE状态的cluster,只有1个SWAPPING状态的cluster。
- 总共只有2个Cluster,没有cluster状态为ACTIVE和SWAPPING,只有1个cluster状态为FULL。
- 没有ACTIVE和SWAPPING状态的Cluster,只有FULL状态的Cluster。
- 其他。
原则上,大于2个Cluster只要有1个ACTIVE或1个SWAPPING状态的Cluster都能被成功初始化,如果存储在Flash中的数据受干扰异常导致无法识别ACTIVE和SWAPPING状态的Cluster,会导致初始化失败,会重新初始化Cluster,历史数据丢失。
写入Block
对外接口:Std_ReturnType Fee_Write(uint8 FeeInstanceId, uint16 BlockNumber, const uint8 *DataBufferPtr, boolean IsSync)
内部接口:MemIf_JobResultType Fee_WriteJob(uint16 BlockIndex, const uint8* DataBufferPtr, uint8 FeeInstanceId)
上层调用Fee写入Block数据,支持异步。
在Fee_WriteJob()需考虑写入时Flash空间不为空的情况,若不为空应该找到空白区域,使下次可正常写入,具体写入流程如下:
- 每次写入前都会检查要写入BlockHeader的地址空间是否为空白,如果不空白,解析BlockHeader。
- 为空,跳出检查。
- DataValid或NumLenValid有效,根据此Block长度跳地址,继续检查下个地址空间是否空白(步骤1)。
- NumLen和Data都无效,跳过一个BlockHeader长度继续检查(步骤1)。
- 写入BlockHeader中的INFO(BlockNumber、BlockLen),写入BlockHeader中的NUMLEN有效标志位。
- 检查要写入数据的区域是否为空,不为空则退出。
- 写入BlockData,写入Data有效标志位。
- 数据写入Flash成功,更新BlockInfo,指向最新的Block地址。
读取Block
对外接口:Std_ReturnType Fee_Read(uint8 FeeInstanceId, uint16 BlockNumber, uint16 BlockOffset, uint8 *DataBufferPtr, uint16 Length, boolean IsSync)
内部接口:MemIf_JobResultType Fee_ReadJob(uint16 BlockIndex, uint16 BlockOffset, uint8* DataBufferPtr, uint16 Length, uint8 FeeInstanceId)
上层调用Fee读取数据,支持异步,读取通过偏移和长度参数,读取Block中的部分数据内容。
Block读取较为简单,判断参数正确未越界即可直接读取Flash中的数据到内存,如果Flash中未保存此数据返回MEMIF_BLOCK_INVALID。
同异步、多实例
Fee和Fls都支持同步和异步的调用方式,为了实现异步调用,需要有一个管理任务的内存空间,Fee和Fls都使用JobInfo结构体来管理。每个实例都有独立的ram空间,所以多实例问题也很好解决。
Fee对外接口中,读Block、写Block、失效Block三个接口支持异步的方式,Fls对外接口中,读Flash、写、擦、比较、空校验这些接口支持异步方式调用。
- 异步调用Fee对外接口,接口函数会检测参数正确性,无误后将任务参数写入JobInfo,Fee状态变为Busy,然后接口return,在Fee_main中执行Job。
- 同步调用Fee读写接口和异步方式相同,写入JobInfo后会在对外接口函数内调用执行Job函数,直到状态变为IDLE。
MemIf
内存抽象接口(MemIf)模块提供对底层Fee或Ea模块的抽象,由NvM调用传入形参实例号(DeviceIndex),MemIf根据实例号区分该调用Fee或是Ea模块。
MemIf也做成了可配置的形式,可通过配置将需要用的子模块链接到程序中,不用的模块不链接,实现整套协议栈功能可裁剪。
NvM
NvM模块包含:下电写入、周期写入、事件触发写入、Block标定、Block清除(恢复默认值)、Block读取写入、Block结构体变更处理、Block校验失败处理、Block数据和RTE同步功能。
NvM是基于原本EPara模块重构的,使用MemIf的接口,对上提供原EPara有的那些服务接口,如标定、同步这些AUTOSAR中没有的功能。其余读写Block,接口改的与AUTOSAR一致,一些AUTOSAR中有的但用不到的接口部分也没有实现。
初始化接口:void NvM_Init( const NvM_ConfigType* ConfigPtr )
流程:校验config参数,读取所有Block,创建NvM周期任务。
读取Block任务接口:static void NvM_ReadBlock(uint16 BlockId)
流程:读取NvM Block Header,根据头中的Block Len读取相应长度的数据段,做CRC16校验。CRC校验失败恢复默认值,版本不一致恢复默认值。原数据长度小于配置的Block长度,继承原先长度的Block数据,超过原先长度的部分从默认值获取。原数据长度大于等于配置的Block长度,保留Block所配置长度的数据段。
写入Block任务接口:static void NvM_WriteBlockJob(uint16 BlockId)
流程:读取Block,比较不一致再写入,写入成功或比较一致清除Block写入标志位。
写入Block接口:void NvM_WriteAll(boolean IsSync)
void NvM_WriteBlock(uint16 BlockId, boolean IsSync)
流程:这两个写入Block接口会将Block写入标志位置写入标志位,如果IsSync = TRUE,会在函数内部调用NvM_ExecuteJob()
,执行写入任务。
周期写入接口:static void NvM_CycleWrite(void)
流程:写入周期到,会对所有配置了周期写入的Block置写入标志位。
下电写入接口:static void NvM_PoweroffWrite(void)
流程:检测到预下电RTE信号上升沿,会对所有配置了周期写入的Block置写入标志位。
事件触发写入接口:static void NvM_EventWrite(void)
流程:检测到对应Block事件触发信号上升沿,会对此Block置写入标志位。
执行任务接口:static void NvM_ExecuteJob(void)
流程:执行写入任务,调用一次ExecuteJob,最多只会写入1个Block。
NvM存储各层数据结构
待改进点
- Fls写入使用异步非阻塞,每写入一个页挂起,需要os支持。
- Fee异常时的擦除也可以挂起,减少阻塞时处理器性能浪费。
- MemIf下可以挂除Fee和Ea外的设备,支持S32K的EEP,因为S32EEP不需要均衡磨损,本身就是用Flash模拟的,所以不适合挂在Ea和Fee。为什么不直接用Flash,然后挂Fee上?因为想让用S32Eep的老项目支持这套内存协议栈。
- NvM作为协议栈的上层,可以实现更多的功能。
思考
整套协议栈自己实现相比直接用AUTOSAR的组件有很多的优点,有哪些自己特殊的需求加到协议栈里比较方便。比如使用之前的均衡磨损算法、NvM支持数据不同版本继承、Fee对数据继承的支持、有同步接口的需求、外部Flash也想使用Fee等等。
调试的过程中云途的mcu擦flash也有坑,这么多个模块开发下来,其中感觉最为复杂的是Fee,完成一个能经得住随机下电,下次上电能自恢复的Fee是很有成就感的。这里要感谢我领导对我的支持与帮助,给我时间让我自我发挥,遇到我解决问题可以一同调试。