1. Cache的不正确使用很容易造成代码执行问题
在协助客户处理问题时经常发现客户在使能指令和数据Cache后,不注意Cache的使用而造成很多代码的运行问题。
2. 曾经遇到过的一个案例
客户在外部使用了3个相同的外设,使用SPI模式进行通讯,调试发现最后一个初始化过的外设总是工作不正常,检查发现对最后这个外设的初始化配置没有生效,造成工作不正常。
经过调试发现客户在使能Cache后,没有进行任何操作,没有配置MPU,也没有对Cache进行冲刷。造成最后一个设备初始化的配置一直没有生效。
3. Cache的使用概念和MPU配置
3.1. Cache的使用概念




Cache的读策略:
对于Cacheable的传输,读首先要在Cache中查找。
如果Hit:则CPU直接从Cache中读取数据即可。
如果Miss,有下面两种处理方式:Read allocate:将读回的数据在Cache中分配一个Cache line进行存储,再从Cache中读数据。占用Cache。Read through(也有称为Read non-allocate):将读回的数据直接给master使用,不在Cache中缓存。避免Cache占用,但是对统一内容连续读效率很低。
默认情况下都是使用read allocate(无论是write-back还是write-through,他们的读取策略是一样的)。
Cache的写策略与write allocate / write non-allocate
Hit时:Write-through:将写的数据更新到Cacheline,同时将这个数据写到内存中。此模式的优点是操作简单,保证数据一致性;缺点是因为数据修改需要同时写入存储,数据写入速度较慢,所需带宽更大。Write-back:直接将数据更新到Cacheline,不写到内存,在这个cacheline被驱逐出Cache时(flush),才写入到内存中。优点是写入速度非常快,多次到Cache的写只需一次到Main Memory的写,所需带宽更小。缺点是Main Memor数据和Cache中不一致,另外Cache替换的读操作会将dirty块写入Main Memory。
Miss时要考虑allocate配置:Write allocate配置时:先在Cache中分配一个cacheline,将数据读到Cache中,然后依照write-back和write-through回写数据(具有write-through策略和writeallocate的缓存在缓存Miss时从内存中读取整个块(Cacheline),并仅将更新的项写入存储的内存中。驱逐不需要写入内存)。Write non-allocate配置:无论write-back还是write-through都直接将数据写到内存中,不会被加载到Cache中。只有读操作会缓存。
3.2. Cache的使用图解
CPU读Cache时:Hit时,直接从Cache中读取。如果miss,则Read through或Read allocate。CPU写时:Hit时使用Write Through或Write-back。如果miss:使用Write allocate或是No write allocate,同时配合Write Through或Write-back策略。
Write策略组合:
Write Through (hit) + Write Allocate(miss)
Write Through (hit) + No Write Acllocate(miss)
Write Back (hit) + Write Allocate (miss)
Write Back(hit) + No Write Allocate (miss)
在有Cache的单机系统中,通常有两种写策略:write through和write back。这两种写策略都是针对写命中(write hit)情况而言的:write through是既写Cache也写main memory;write back是只写Cache,并使用dirty标志位记录Cache的修改,直到被修改的Cache块被替换时,才把修改的内容写回main memory。那么在写失效(write miss)时,即所要写的地址不在Cache中,该怎么办呢?一种办法就是把要写的内容直接写回main memory,这种办法叫做no write allocate policy;另一种办法就是把要写的地址所在的块先从main memory调入Cache中,然后写Cache,这种办法叫做write allocate policy。
读命中(Hit):

Read Allocate(读分配,先把数据读到Cache中,再从Cache中读到CPU。Cache使能时,默认都是使用read allocate):

Write Through(WT,写通,透写,直接写,Hit时把数据同时写到Cache和内存中):

写数据时同时更新缓存和二级存储,缓存行不被标记为“dirty。这样,这对于数据一致性来说更安全,但它需要更多的总线访问。当某一个Cache line需要替换时,就不必将其中的数据写到主存储器中去了,新调入的块可以立即把这一块覆盖掉。
尽管由于总线流量的增加,Write-Through缓存策略使用更多的能量并且速度更慢,但与使用回写缓存策略相比,它们有两个优点:
Write-Through Cache policies reduce the need for software Cache maintenance.
Write-Through Cache policies avoid the extra decrease in determinism because there is never any dirty data in the Cache.
Write Back(hit时):
写数据时,只更新缓存,然后将Cache line标记为“dirty”,当这个缓存行被新的缓存行替换,或者手动clean的时候,再将数据写到存储器中。

Write Allocate(WA,写分配,Miss时考虑):
当进行数据写操作时,如果Cache未命中,Cache系统将会进行Cache内容预取,从主存中将相应的块读取到Cache中相应的位置,并执行写操作,把数据写入到Cache中。对于写通类型的Cache,数据将会同时被写入到主存中,对于写回类型的Cache数据将在合适的时候写回到主存中(写在此时只发生在Cache中)。
由于写操作分配Cache增加了Cache内容预取的次数,它增加了写操作的开销,但同时可能提高Cache的命中率,因此这种技术对于系统的整体性能的影响与程序中读操作和写操作数量有关。

Write non-Allocate(Miss时考虑):Cache miss时,无论是write through还是write back,都是直接将数据写入内存中。不加载到Cache。
3.3. Cache控制寄存器


SIWT=0:所有的shared位置被认为是Non-cacheable的(例如Dcache),配置的内部可缓存性属性将被忽略掉。(share了的就不能Cache,这是共享内存的默认操作模式。Cache对这些位置的软件是透明的,因此不需要软件维护来保持一致性。)
SIWT=1:普通的、可缓存的、共享的位置被视为WT(write-through),配置的其他属性不受CACR.SIWT的影响,仍然是WT特性。对于I-cache:共享(shared)位置被视为不可缓存(non-cacheable)。
3.4. MPU的使用概念
在STM32H7的PM0253编程手册中MPU配置:


上表中关于Cache策略的设置AA/BB定义如下:
00 Non-cacheable
01 Write-back, write and read allocate

10 Write-through, no write allocate
11 Write-back, no write allocate
概念:TEX,C,B,S, memory type,shareability,attributes,Write Through,Write Back,Write Allocate,No Write Allocate,Read Allocate
内存类型:Normal Memory, Device Memory, Strongly Ordered Memory。Device和Strongly-ordered类型的存储器(可以理解为外设)区域,都是Noncacheable的。见上表“Other attributes”栏。
缓存策略cacheability:Non-Cacheable(主控不使用Cache,直接与存储器交互,这样不会带来内存一致性问题,但是效率不高)和Cacheable(Cacheabe又分为Write-Through Cacheable透写和Write-Back Cacheable回写)
共享属性shareability:共享Shareable(比如多核CPU中,UART外设地址空间是对所有CPU共享的),不共享Non-shareable(当被配置成为Non-Shareable的时候,意味着在多核系统中,它只能被一个核心访问)
Normal Memory:通常,用于程序代码和数据存储的内存是普通内存。使用普通内存技术的例子有:
Programmed Flash ROM/*在编程过程中,闪存可以比普通存储器更严格地排序*/ROM、SRAM、DRAM和DDR memoryDevice Memory:Peripherals and I/O,通常是系统外设(I/O),Device内存类型属性定义了内存位置,在这些位置上,对这些位置的访问可能会导致副作用,或者为一个负载返回的值可能会根据执行的负载数量而变化。内存映射外设和I/O位置是通常标记为设备的内存区域的示例:memory-mapped外设(比如I/O,访问会造成一些影响。),内存控制配置寄存器,中断控制寄存器,FIFO。
Strongly Ordered Memory:比Normal Memory的访问规则更严格;Peripherals and I/O,通常是系统外设(I/O),对于强顺序内存,所有内存访问都严格按照内存访问指令的程序顺序进行排序:读和写都会对其他方面造成一定影响;访问不能重复;必须维护访问的数量、顺序和大小。具有Shareable特性。
3.5. ST的参考文件
AN4839:Level 1 Cache on STM32F7 Series and STM32H7 Series
AN4838:Introduction to memory protection unit management on STM32 MCUs
下图是MPU没使能,并且没有对某些特殊寄存器进行配置,那么存储器的映射地址及其属性就可以参考下图。
其中,WT表示Write-through(透写),WB表示Write-back(回写),WA表示Write-allocate(写分配),没有明确标注WA的就是RA(读分配)。

▲ 图. AN4839中STM32F7和STM32H7 default settings
请参考AN4839文件末尾的Cache和MPU使用示例(下图):PC将Flash中的数据拷贝到RAM,DMA将SRAM中的数据拷贝到DTCM中,最后比较DTCM和Flash中的原始数据来理解Cache、MPU和数据一致性。

数据传输中如果配置为write-back,即写数据时,只更新缓存,然后将Cache line标记为“dirty”,当这个缓存行被新的缓存行替换,或者手动clean的时候,再将数据写到存储器中,这样容易造成数据的不一致。
要避免这种问题,需要:

Note:MPU配置为shareable,相当于没使用Cache的效果。
如果系统对执行速度要求比较高,建议使用Cache,然后通过Cache操作函数维护数据的一致性。
AN4838:MPU寄存器的使用信息,可以参考AN4838文件中提到的Programmer manual:

具体配置和示例代码
在AN4838中有如下示例代码:

▲ 图. Example of setting up the MPU

▲ 图. Setting the MPU with STM32Cube HAL
最高性能配置:MPU enable,MPU_TEX_Level1,bufferable,Cacheable,not shareable
3.6. MPU配置和Cache相关函数
使能Cache的函数:
SCB_EnableDCache();
SCB_EnableICache();
使能Cache后,要配合MPU的正确配置来使系统性能最佳。
3.6.1. MPU配置示例:
内部Flash:仅运行指令,最低性能的配置,读写都不使用Cache:
MPU_ACCESS_NOT_BUFFERABLE, MPU_ACCESS_NOT_CACHEABLE, MPU_ACCESS_SHAREABLE, MPU_TEX_LEVEL1. TEX=0b001, C=0, B=0, S=0 or 1.
最高性能的配置:
MPU_ACCESS_BUFFERABLE, MPU_ACCESS_CACHEABLE, MPU_ACCESS_NOT_SHAREABLE, MPU_TEX_LEVEL1。TEX=0b001, C=1, B=1, S=0, shareability =Not shareable, -> outer and inner write-back, write and read allocate.
Flash高性能配置*/
MPU_InitStruct.Enable = MPU_REGION_ENABLE; //使能该保护区域 MPU_InitStruct.BaseAddress = 0x08000000; //设置基地址 MPU_InitStruct.Size = MPU_REGION_SIZE_2M; //保护区域的大小 MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; //设置访问权限 MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; // 允许缓冲 buffer MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; //允许 Cache MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; // 不共用 not share MPU_InitStruct.Number = MPU_REGION_NUMBER0; //设置包含区域 MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; //设置类型扩展域 MPU_InitStruct.SubRegionDisable = 0x00; //禁止子区域 MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; //允许读取指令 HAL_MPU_ConfigRegion(&MPU_InitStruct);
/*DTCM和ITCM,和CPU主频一样,不需要配置MPU和Cache。
AXI SRAM:配置为Normal,Cache读写都开启;性能最强,缺点:由于开启了读Cache和写Cache,需要用户调用函数SCB_CleanInvalidataDCache(),管理数据一致性,需要注意,只有多主控操作(包括DMA)此空间需要如此处理。*/
MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct);
/*对于Device或Strongly order的外设,读写Cache都要关闭,比如寄存器,FMC扩展IO,FMC驱动的一些strongly order外设,LCD,并口NOR Flash,并口SRAM,NAND Flash等。
能保证严格按照程序代码执行,缺点是不支持非对齐访问。
示例代码如下:*/
MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER2; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct);
/*配置为Normal,关闭写Cache,开启读Cache,特点:保证数据直接写到SDRAM,保证读性能最佳。缺点:由于开启了读Cache,需要用户调用函数SCB_CleanInvalidataDCache(),管理数据一致性问题,这里需要注意,只有多主控操作此空间需要处理(包括DMA)。*/
MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_32MB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER3; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct);
/*配置为最低性能Normal,关闭Cache的读写,normal里性能最差*/
MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER5; //根据具体配置Number号 MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct);
MPU的详细内容,请参考ARM使用手册DUI0646C_cortex_m7_dgug(Arm Cortex M7 Devices Generic User Guide r1p2,下面是Cortex-M7 MPU的寄存器:

3.6.2. Cache相关函数的使用说明
CMSIS软件包的core_cm7.h文件为Cache的配置提供了11个函数:
SCB_EnableICache
SCB_DisableICache
SCB_InvalidateICache
SCB_EnableDCache
SCB_DisableDCache
SCB_InvalidateDCache
SCB_CleanDCache
SCB_CleanInvalidateDCache
SCB_InvalidateDCache_by_Addr
SCB_CleanDCache_by_Addr
SCB_CleanInvalidateDCache_by_Addr
SCB_EnableICache(void) 使能 I Cache
SCB_DisableICache(void) 失能 I Cache
SCB_InvalidateICache(void) 使 I Cache line标记为无效,等同于删除操作。当读取指令时,会理解成Cache-line中没有对应指令,转而去真实的物理地址获取指令。
SCB_EnableDCache(void) 使能D Cache
SCB_DisableDCache(void) 失能D Cache
SCB_InvalidateDCache(void) 使D Cache标记为无效,等同于删除操作,当读取数据时,会理解成Cache-line中没有对应数据,转而去真实物理地址获取数据。
SCB_CleanDCache(void) Clean所有的Cache-line,即将标记为dirty的Cache-line数据全部写到Cache line对应的真实的物理地址中。(比如write-back,Cache-line中有新的数据,并且标记为dirty,但真实物理地址中可能是旧的数据。这个clean,会把数据回写到真实memory中。)
SCB_CleanInvlaidateDCache(void) 是前面两个函数SCB_InvalidateDCache和SCB_CleanDCache的二合一。将Cache Line中标记为dirty的数据写入到相应的存储区后,再将Cache Line标记为无效,表示删除。Cache空间就都腾出来,可以加载新的数据。
SCB_InvalidateDCache_by_Addr() 根据地址信息无效其对应的Cache-line。
SCB_CleanDCache_by_Addr() 根据地址信息clean其对应的Cache-line。
SCB_CleanInvalidateDCache_by_Addr() 根据地址信息clean并invalidate其对应的Cache-line。
Cortex-M7内核的L1 Cache由多行内存区组成,每行有32字节,请注意Cache addr操作的地址一定要32字节对齐的。dsize一定要是32字节的整数倍。(Cortex m7 Cache line是8 words,32bytes。对应的如果其他系统是64bytes Cache line对应的就是64bytes)
3.6.3. 使用DMA时软件维护一致性
启用Cache通常可以带来程序运行速度的提升,不过,当在代码中使用到DMA时,我们可能需要根据DMA的不同传输方向,采取相应Cache维护策略措施以保持多主访问时的数据一致性。
在启用Cache并遵循32字节对齐的条件下:
如果DMA负责从I/O读取数据到内存(DMA Buffer)中,那么在DMA传输之前,可以invalid DMA Buffer地址范围的高速缓存。在DMA传输完成后,程序读取数据不会由于Cache hit导致读取过时的数据。
如果DMA负责把内存(DMA Buffer)数据发送到I/O设备,那么在DMA传输之前,可以clean DMA Buffer地址范围的高速缓存,clean的作用是写回Cache中修改的数据(数据准备阶段用到的Cache line)。在DMA传输时,就不会把主存中的过时数据发送到I/O设备。
请注意,在DMA传输没有完成期间CPU不要访问DMA Buffer(如果需要访问DMA buffer,请注意保证数据的一致性)。在速度要求不太严格的时候,保险起见(比如数据没有对齐,或是客户需要使用简单方法保证数据一致性),建议DMA传输前,对DMA要访问的对应地址的Cache空间进行先clean后invalid操作(先clean不会损坏其他有用数据,后invalid操作可以理解为删除操作)。在DMA传输完成,并搬移DMA buffer内容到其他空间后,对对应地址的Cache空间进行先clean后invalid操作。当然,本建议可根据具体应用灵活处理。
Cache addr操作的地址一定要32字节对齐的。dsize一定要是32字节的整数倍。这点同样适用于可以Cacheable的DMA buffer(dsize为32byts整数倍,当buffer大小不是32bytes的整数倍的情况下有一定的空间浪费)。32bytes是根据Cache line的最小粒度来确定的(Cortex m7 Cache line是8 words,32bytes。对应的如果其他系统是64bytes Cache line对应的就是64bytes)。如果DMA Buffer的安排也遵守32字节对齐,就可以避免同一Cacheline出现两种应用场景的数据,否则不方便针对Cacheline做数据维护操作。
Cache的使用能否提高效率需要综合考虑,因为维护数据一致性有一定开销:对于32bytes,16bytes,8bytes,1bytes的一些常用变量,不使用Cache但把他们放在0延时等待的DTCM空间效率会更好。对与512bytes,1024bytes等,空间需求较大,DTCM放置栈、堆及常用变量后可能空间比较紧张了,这些需求量大的buffer可以放在AXI SRAM,SRAM1,SRAM2等空间,使用Cache效率会更高,但需要地址和size大小对齐。配置外设、同外设进行通讯、DMA操作时也需要维护一致性或是配合MPU来使用。
4. 小结
在使用Cache时要配合使用MPU,并且做好数据一致性的处理。
来源:STM32
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。