Cortex-M7 对于 指令乱序执行特性, Cache, 以及写代码时如何应对这些特性

本文详细介绍了Cortex-M7处理器的乱序执行特性、L1 Cache及其对内存访问的影响,强调了内存分类(Normal、Device、Strongly-ordered)和访问顺序的重要性。通过使用DMB、DSB和ISB指令,以及MPU配置,可以确保外设寄存器和DMA操作的正确顺序。同时,文章还探讨了如何处理数据一致性问题,包括Cache同步方法和MPU在确保数据一致性和访问权限的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

Cortex-M7相对于之前的M4,M3有很大区别,尤其是指令流执行方面。M7拥有6级超标量流水线,对于它到底有多少级,我们不需要太关心。我们需要真正注意的是它具有指令乱序执行的特性,这个玩意不处理好的话会导致一些奇奇怪怪的问题。
除此之外,M7还具有L1 Cache,这个会引入数据一致性问题,也是个麻烦的东西。
(好家伙,CPU乱序执行,Cache搞假数据)

Cortex-M7的乱序执行

关于乱序执行,直接以关键字搜索“CPU乱序执行”可以找到很多相关内容,但我们不需要对其中的实现细节深入了解。只需要知道它是为了效率而设计,并且它会在一些时候提前执行一些代码。

实际上由于硬件限制,CPU不能非常乱的执行代码,但我们可以认为它是完全无序的,它只保证某些内存访问顺序是正确的,而我们如何控制哪些内存是顺序访问的就是我们的目的。(例如外设寄存器访问,DMA要操作的内存等,这些都需要保证顺序,至少一定程度上需要保证顺序)

相反的,编译器编译出的代码更像是乱序执行,keil中在魔术棒里配置的O0优化和O3在调试时完全是两个感觉(谁试谁知道)

像STM32H750,它可以通过MPU将内存配置为Device或者Strongly-ordered类型,这样Cortex-M7访问这些内存时,会严格保证顺序,不会乱来,而对于一些内存,例如堆栈或者一些与硬件无关(一般只有CPU访问的内存都是)的内存,则可以配置为Normal,Cortex-M7将毫无顾忌的访问执行内存,内存访问的顺序将与代码的逻辑执行顺序不同,以提高效率。

逻辑依赖

不禁想到,如果CPU乱序执行代码,不会乱套吗?我们先看看程序的定义:

实际上,对程序的定义有一点是程序必须要有输出,如果没有输出,那它就不能算程序。为什么,如果一个所谓的“程序”没有输出,那它完全可以什么都不做(表现就是编译器把代码全部优化没了),因为我们(用户)不需要从程序中获取任何信息(即输出),CPU完全可以偷懒什么都不干。(编译器: 我刚刚把一段程序执行时间优化到了0,厉害吧)
即使编译器没有开启优化,如果CPU的流水线是理想的(级数无限),CPU完全可以判断这些指令不需要执行,直接跳过了。

看了前面的内容,下面的就好理解了。

无论是乱序执行(CPU执行代码时发生),还是编译器优化(编译代码时发生),必须保证输出不能有任何改变(不然我代码写了个寂寞)
我管这叫逻辑依赖

可以看出,无论是乱序执行还是编译器优化,都是"乱中有序"的。能乱,但不能乱来。

这样,就出现了一个新问题,如何定义哪些是输出/输入呢?

什么?多了个输入,没错,输入是程序的可选项,并且,它也享受和输出一样的优待,即不优化保证顺序等。

编译器乱序

其实我们早在Cortex-M3,Cortex-M4的代码编写时,就已经碰到这个问题了。最突出的就是volatile关键字,很多寄存器定义都需要它,但它的作用很简单,将C代码对这些变量的操作(每次的操作)都落实到指令级别的内存访问(对于Cortex 就是:LDR,STR等指令),防止使用这些变量时引用寄存器中的副本,并且,多个volatile之间的执行顺序(编译后机器码中的执行顺序)也可以得到编译器的保证,不会被优化乱套,这就有点输入/输出的意思了。
除了volatile,在MDK中,还有一个伪函数: __schedule_barrier()
如果说volatile是用来保证内存的访问操作和顺序,那__schedule_barrier()就是用来保证代码的执行顺序(代码中上在__schedule_barrier()之前的指令在编译后仍然在之前)。对于一些普通的代码无需用它,因为输入/输出一般都以内存访问的形式出现在编程中,使用volatile就足够了;但是,对于Cortex,它的关闭中断操作不是内存操作,而是一个指令,这是,如果想要保证某些代码在关闭中断指令后执行,需要使用__schedule_barrier()在关闭中断指令之后,其他后续指令之前。

在Cortex-M3,Cortex-M4中,由于没有指令乱序执行的特性,所以我们只需要使用保证volatile__schedule_barrier()保证指令顺序就不会出问题了。

但在Cortex-M7中,事情变得有些不同,CPU会乱序执行代码,光保证代码顺序已经不够了。

Cortex-M7 对内存的分类和访问操作

对于这些内容,在ST官网有个文档PM0253描述了这些,包括MPU,Cache操作等,这里截取一些并进行说明。

内存类型

内存类型分布
这个表对Cortex-M7地址空间的内存类型进行了描述,这些内存类型可以通过MPU设置进行覆盖。

可以看出,内存总共有3种类型:
Normal

普通内存,乱序执行可以为所欲为

Device

一般用于外设的寄存器,保证访问的顺序,但可能推迟或提前(和C中的volatile很像),写操作可缓冲。

Strongly-ordered

Device类似,但它更为严格,不可缓冲。(与指令同步?)

可以发现,外设寄存器和其他一些非内存的玩意都不为Normal,这相当于CPU中的"volatile"了,并且Strongly-ordered"volatile"更严格。

访问可见性

访问可见性0
访问可见性1

上图说明了一些访问顺序的依赖(在没有代码依赖的情况下),标记为-的,CPU就可能会乱序进行访问。

规避负面影响

为了在某些特殊的场合下,避开这些机制产生的影响(不确定性),例如我想让某个外设寄存器(Device)操作必须在某一个内存(Normal)操作之后。

Cortex-M7(其实M4, M3 就已经有了)提供了3个用于数据同步的指令。提供CMSIS可以这么调用:

__DMB()
数据内存屏障,使用该指令,可以保证前面的内存访问操作完毕后才进行后续的内存访问。(对于有Cache的情况,该指令并不会导致Cache与内存之间的内存同步,下面的指令同)
__DSB()
数据同步屏障,相比DMB更严格,可以保证前面的内存访问操作完毕后才进行后续的指令执行。 不仅后续的内存访问不会提前,就连指令也不会。
__ISB()
这个似乎比较特殊,它会清除指令流水线中的所有指令,使得流水线重新从内存或Cache中读取数据。

利用这些指令,可以防止某些场合下乱序执行的影响。

下面是一个STM32H750VB写入内部Flash的操作例子,由于Flash写入是写入的内存类型是Normal,而FLASH的相关寄存器是Device,M7默认认为它们之间是无逻辑依赖的,所以某些位置需要调用同步指令,由于所有操作都是基于内存访问的,只用DMB即可满足要求:

/// <summary>
/// 写入flash 无需32字节对齐 自动补齐
/// </summary>
/// <param name="programAddr">编程地址</param>
/// <param name="data">源数据</param>
/// <param name="SizeInByte">数据大小</param>
/// <returns>0:成功</returns>
int Flash_Bank1_Program(unsigned int programAddr,void *data,unsigned int SizeInByte){
  int i;
  unsigned int topNum;
  unsigned int heard[8];
  unsigned char *heard_b;
  unsigned char *src_b;
  
  if(FLASH->CR1 & FLASH_CR_LOCK){
    if(Flash_Bank1_UnLock()){
      return -1;
    }
  }

  //等待所有操作完成
  while(FLASH->SR1 & FLASH_SR_QW){
  }

  topNum=programAddr%32;
  topNum=32-topNum;
  if(topNum && (topNum!=32)){

    programAddr=programAddr-programAddr%32;
    heard[0]=((volatile unsigned int *)programAddr)[0];
    heard[1]=((volatile unsigned int *)programAddr)[1];
    heard[2]=((volatile unsigned int *)programAddr)[2];
    heard[3]=((volatile unsigned int *)programAddr)[3];
    heard[4]=((volatile unsigned int *)programAddr)[4];
    heard[5]=((volatile unsigned int *)programAddr)[5];
    heard[6]=((volatile unsigned int *)programAddr)[6];
    heard[7]=((volatile unsigned int *)programAddr)[7];
    __DMB();
    
  }
  else{
    topNum=0;
  }


  FLASH->CR1|=FLASH_CR_PG;
  __DMB();

  src_b=(unsigned char *)data;
  heard_b=(unsigned char *)heard;


  if(topNum){
  
    

    for(i=0; i<topNum; i++)
    {
      heard_b[32-topNum+i]=src_b[i];
      SizeInByte--;
      
      if(SizeInByte==0){
        break;
      }
    }

    
    ((volatile unsigned int *)programAddr)[0]=heard[0];
    __DMB();
    ((volatile unsigned int *)programAddr)[1]=heard[1];
    __DMB();
    ((volatile unsigned int *)programAddr)[2]=heard[2];
    __DMB();
    ((volatile unsigned int *)programAddr)[3]=heard[3];
    __DMB();
    ((volatile unsigned int *)programAddr)[4]=heard[4];
    __DMB();
    ((volatile unsigned int *)programAddr)[5]=heard[5];
    __DMB();
    ((volatile unsigned int *)programAddr)[6]=heard[6];
    __DMB();
    ((volatile unsigned int *)programAddr)[7]=heard[7];
    __DMB();

    src_b=&src_b[topNum];
    programAddr+=32;
  }


  while(SizeInByte>=32){
  
    for(i=0; i<32; i++)
    {
      heard_b[i]=src_b[i];
    }
    
    ((volatile unsigned int *)programAddr)[0]=heard[0];
    __DMB();
    ((volatile unsigned int *)programAddr)[1]=heard[1];
    __DMB();
    ((volatile unsigned int *)programAddr)[2]=heard[2];
    __DMB();
    ((volatile unsigned int *)programAddr)[3]=heard[3];
    __DMB();
    ((volatile unsigned int *)programAddr)[4]=heard[4];
    __DMB();
    ((volatile unsigned int *)programAddr)[5]=heard[5];
    __DMB();
    ((volatile unsigned int *)programAddr)[6]=heard[6];
    __DMB();
    ((volatile unsigned int *)programAddr)[7]=heard[7];
    __DMB();

    src_b=&src_b[32];
    programAddr+=32;
    SizeInByte-=32;
  }

  if(SizeInByte){
  
    heard[0]=((volatile unsigned int *)programAddr)[0];
    heard[1]=((volatile unsigned int *)programAddr)[1];
    heard[2]=((volatile unsigned int *)programAddr)[2];
    heard[3]=((volatile unsigned int *)programAddr)[3];
    heard[4]=((volatile unsigned int *)programAddr)[4];
    heard[5]=((volatile unsigned int *)programAddr)[5];
    heard[6]=((volatile unsigned int *)programAddr)[6];
    heard[7]=((volatile unsigned int *)programAddr)[7];
    __DMB();

    for(i=0; i<SizeInByte; i++)
    {
      heard_b[i]=src_b[i];
    }

    ((volatile unsigned int *)programAddr)[0]=heard[0];
    __DMB();
    ((volatile unsigned int *)programAddr)[1]=heard[1];
    __DMB();
    ((volatile unsigned int *)programAddr)[2]=heard[2];
    __DMB();
    ((volatile unsigned int *)programAddr)[3]=heard[3];
    __DMB();
    ((volatile unsigned int *)programAddr)[4]=heard[4];
    __DMB();
    ((volatile unsigned int *)programAddr)[5]=heard[5];
    __DMB();
    ((volatile unsigned int *)programAddr)[6]=heard[6];
    __DMB();
    ((volatile unsigned int *)programAddr)[7]=heard[7];
    __DMB();

    src_b=&src_b[SizeInByte];
    programAddr+=SizeInByte;
    //SizeInByte-=SizeInByte;
    SizeInByte=0;
  }
  
  if(FLASH->SR1 & FLASH_SR_WBNE){
    FLASH->CR1 &= FLASH_CR_FW;
  }

  //等待所有操作完成
  while(FLASH->SR1 & FLASH_SR_QW){
  }

  FLASH->CR1 &= ~FLASH_CR_PG;

  return 0;
}

实际上,大部分操作(有关与外设的)都是基于内存访问的(例如外设寄存器,内存等),这些情况下的同步只需要DMB指令就能同步了。我发现需要DSB的例子就只有关中断指令,如果操作会导致代码区更新,并且可能调用其中的代码,则需要ISB指令

Cortex-M7 中的Cache

在Cortex-M7与AXIM之间,多了个Cache,在它带来性能提升的同时,也有一些麻烦的事情。

数据一致性

Cache Cache Cache,它就是个缓存,缓存对内存的操作,它速度很快,当对一小部分内存进行密集(操作频率高)的访问时,很多中间的读写都不会落实到内存中,而是暂存在cache中。虽然速度快了,当缺点很明显,就是读写的内容很可能并不会落实到内存中,如果整个单片机只有CPU操作内存就非常好,但外设(例如stm32中的DMA, ETH USBHS的DMA等)访问内存得到的数据可能就是旧的数据,新的数据还在Cache里呢,相反的,外设向内存写入的数据可能不会被CPU读取到,因为CPU很可能读取的是Cache里的数据了。

如何Cache同步

所幸,这些不是不可避免的。
在头文件core_cm7.h中,有一些方法可用于数据同步。

对于指令Cache(ICache)同步,只有一个方法提供

void SCB_InvalidateICache (void)
无效化ICache,将ICache内缓存的指令无效化。
例子: 这个可以用在将Flash内容修改后使用,使得将要执行的指令是新的。

对于数据Cache(DCache)同步,相关的同步方法有很多

void SCB_InvalidateDCache (void)
无效化整个DCache

void SCB_CleanDCache (void)
将缓存的并修改过的数据同步写入到内存

void SCB_CleanInvalidateDCache (void)
将缓存的并修改过的数据同步写入到内存后无效化整个DCache

void SCB_InvalidateDCache_by_Addr (uint32_t *addr, int32_t dsize)
SCB_InvalidateDCache的内存区域版本,将操作涉及到的CacheLine
如果要同步的内存区域很小,这个方法很快

void SCB_CleanDCache_by_Addr (uint32_t *addr, int32_t dsize)
SCB_CleanDCache 的内存区域版本,将操作涉及到的CacheLine
如果要同步的内存区域很小,这个方法很快

void SCB_CleanInvalidateDCache_by_Addr (uint32_t *addr, int32_t dsize)
SCB_CleanInvalidateDCache 的内存区域版本,将操作涉及到的CacheLine
如果要同步的内存区域很小,这个方法很快

对于内存区域版本,它的效率低于整个操作Cache的分界线应该在要操作的内存区域大小接近于Cache大小附近(对于STM32H750,如果要同步的内存大小远大于16KB,使用内存区域版本的同步方法就效率更低了,因为这会花更多的时间写入同步地址寄存器(SCB->DCIMVAC,SCB->DCCMVAC,SCB->DCCIMVAC))

对于这些方法的使用,用户无需操心调度屏蔽,乱序执行的影响,这些工作在这些方法内部完成了。
void SCB_InvalidateICache (void)举例:

#define __ISB() do {\
                   __schedule_barrier();\
                   __isb(0xF);\
                   __schedule_barrier();\
                } while (0U)


#define __DSB() do {\
                   __schedule_barrier();\
                   __dsb(0xF);\
                   __schedule_barrier();\
                } while (0U)


#define __DMB() do {\
                   __schedule_barrier();\
                   __dmb(0xF);\
                   __schedule_barrier();\
                } while (0U)

__STATIC_INLINE void SCB_InvalidateICache (void)
{
  #if defined (__ICACHE_PRESENT) && (__ICACHE_PRESENT == 1U)
    __DSB();
    __ISB();
    SCB->ICIALLU = 0UL;
    __DSB();
    __ISB();
  #endif
}

调度屏蔽,指令同步安排得明明白白。
可以发现__schedule_barrier()已经被安排进去了,DSB,ISB这种指令可不能被编译器乱排了。(因为编译器可能不知道这种指令是需要注意的)

数据同步时,要注意对齐同步问题,Cache最小单元为32字节,并且同步内存块的首地址都为32的倍数(32字节对齐),如果要同步的数据首地址和尾部有不对其32字节边界的情况的话,这种情况就要小心了,例如: 如果用户的普通数据和DMA要操作的数据的在同一个32字节块里的话,仅Invalidate会使修改过(如果修改过)的用户数据丢失,但CleanInvalidate会破坏DMA传输好的数据,所以,涉及外设的内存有两种处理方式:
1.通过MPU配置内存为不缓存,不缓冲。
2.如果对于内存区域开启了可缓存或可缓冲,则保证内存区域占用的32Byte块(一般为开头或结尾的)中无其他的用户数据,防止相互妨碍。

MPU

前面提到,内存分为好几种类型,但MPU除了能覆盖掉默认的内存类型外,还能进行Cache策略配置,读写权限配置等。

NPU0
MPU1
MPU3
缓存策略通过TEX C B 来配置。一般使用TEX=0 =1这两种TEX。
从表中可以得知各种策略下的访问Cache策略。

<think>嗯,用户想了解STM32与缓存相关的概念或解决STM32缓存问题。首先,我需要回忆一下STM32的体系结构。STM32系列主要基于ARM Cortex-M内核,不同的型号可能使用不同的内核,比如M3、M4、M7等。其中,Cortex-M7是支持缓存的内核,比如STM32F7和H7系列。其他如M3、M4通常没有硬件缓存,这可能是一个关键点。 用户可能遇到的问题可能是在使用某些高性能应用,比如图形库LVGL或者需要大量数据处理,遇到性能问题或者内存一致性问题。比如在引用中提到的栈空间不足导致hard_fault,这可能与内存管理有关,但缓存问题可能出现在数据访问速度或一致性上。 接下来,需要解释STM32中的缓存机制。Cortex-M7指令缓存(I-Cache)和数据缓存(D-Cache)。缓存的作用是加速访问,但也会带来一致性问题,比如DMA传输的数据可能被缓存,导致CPU看不到最新数据,或者CPU修改的数据未及回内存,DMA传输旧数据。这候需要软件维护缓存一致性,比如使用SCB_CleanDCache、SCB_InvalidateDCache等函数。 然后,需要讨论常见问题及解决方法。例如,使用DMA,如果缓存未清理或无效化,可能导致数据不一致。此需要在使用DMA前清理缓存(确保数据入内存),或者在DMA传输后无效化缓存(确保CPU读取最新数据)。另外,内存对齐问题也可能影响缓存效率,特别是在使用MPU(内存保护单元)配置缓存策略,可能需要设置内存区域的缓存属性。 此外,用户可能在移植LVGL这类图形库遇到性能问题,启用缓存可以提升性能,但必须处理好一致性问题。比如在STM32H7上,启用D-Cache后,LVGL的帧缓冲区需要配置为回模式,并在DMA传输前清理缓存,确保图像数据正确显示。 还需要考虑MPU的配置,不同的内存区域(如内部SRAM、外部SDRAM)可能需要不同的缓存策略,比如关闭缓存、透模式或回模式。配置错误可能导致数据错误或崩溃。 最后,生成相关问题要覆盖缓存的作用、常见问题、优化方法、MPU配置以及具体应用场景如LVGL中的使用。确保用户能全面了解STM32缓存相关的内容,并解决实际开发中的问题。</think>### STM32缓存相关概念与问题解析 STM32系列微控制器基于ARM Cortex-M内核,其中部分型号(如Cortex-M7内核的STM32H7/F7系列)包含**指令缓存(I-Cache)**和**数据缓存(D-Cache)**。缓存主要用于加速对内存的访问,但在使用中可能引发一致性问题。以下是关键点: --- #### 1. 缓存的作用与配置 - **指令缓存(I-Cache)**:加速代码执行,减少从Flash读取指令的延迟。 - **数据缓存(D-Cache)**:加速数据读,降低对RAM或外部存储器的访问频率。 - **启用缓存**:在系统初始化通过`SCB_EnableICache()`和`SCB_EnableDCache()`启用[^1]。 --- #### 2. 缓存一致性问题 当CPU与DMA或其他外设共享数据,缓存可能导致**数据不一致**: - **DMA传输前**:若CPU修改了缓存中的数据,需调用`SCB_CleanDCache()`将缓存数据回内存,确保DMA获取最新数据。 - **DMA传输后**:若DMA更新了内存数据,需调用`SCB_InvalidateDCache()`使缓存失效,强制CPU重新读取内存。 --- #### 3. 常见问题与解决方法 - **HardFault异常**: 若缓存配置错误(如未正确维护一致性),可能导致内存访问冲突。需检查DMA与缓存的交互逻辑,并添加清理/失效操作。 - **性能瓶颈**: 在图形处理(如LVGL)或高频数据处理,启用缓存可显著提升性能。例如,LVGL的帧缓冲区建议配置为**回模式**,并通过MPU设置内存区域属性[^2]。 - **MPU配置**: 使用内存保护单元(MPU)定义不同内存区域的缓存策略(如关闭缓存、透模式)。例如,外设寄存器区域需关闭缓存以避免副作用。 --- #### 4. 示例代码(缓存维护) ```c // DMA传输前清理缓存 SCB_CleanDCache_by_Addr((uint32_t*)buffer_addr, buffer_size); // 启动DMA传输 HAL_DMA_Start(...); // DMA传输后失效缓存 SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_addr, buffer_size); ``` --- #### 5. 移植LVGL的缓存优化 在STM32H7上运行LVGL: 1. 启用D-Cache并配置帧缓冲区为**回缓存**。 2. 在LVGL刷新显示前,调用`SCB_CleanDCache_by_Addr()`确保帧数据已入内存。 3. 调整栈空间(如引用中提到的栈溢出问题)。 ---
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值