Q:我是MCU开发者,内存屏障和我有关吗?
A:有关!
————————————————
Q:我不使用实时操作系统,只用Bare metal(裸跑)单线程,内存屏障和我有关吗?
A:有关!
————————————————
是不是有点不可思议?
现实存在的问题
说起内存屏障(Memory Barrier),小编也总觉得和MCU的开发者相距甚远,比起LINUX内核的各种SMP、MMIO内存屏障的普遍应用来,MCU中内存屏障的使用少的可怜,MCU的开发者也基本不太重视内存屏障的问题,甚至没有意识到问题的存在。
然而,在最近遇到的MCU问题中,内存屏障问题,尤其是编译时内存重排序问题在使用GCC工具链的MCU开发中反复出现,而广大的开发者往往没有处理过这种问题而根本没有往这方面想,耗费了大量的调试时间。
内存访问指令的重排序
内存排序问题指的是CPU(ARM内核)访问存储器的顺序。
你在电脑上敲下的C代码经过编译变为汇编指令,直到汇编指令在CPU中运行,这整个过程中,代码访问内存的顺序有可能多次被重排序。这些重排序有可能是编译器在编译时导致产生汇编指令的重排序,也可能是CPU执行指令时产生的重排序。这些重排序旨在减少流水线延迟从而提高CPU的性能,当编译器或者CPU发现内存数据访问没有相关性时,这些重排序就有可能发生。
我们下面分别讲一下编译时的内存访问重排序和运行时的内存访问重排序。
编译时的内存访问重排序
我们知道,编译器做的工作是将人类可读的源码转换为CPU可读的指令,这个转换过程有一定的自由度,尤其是编译器优化等级较高时,编译器会分析依赖关系进行指令重排序。
依赖关系分为数据依赖和控制依赖,小编这里就不太多涉及理论了,只讲一个编译时重排序导致程序问题的例子。
Chip_DMA_Table是DMA的描述符数组,下面程序将串口DMA描述符赋值,并将配置DMA串口通道0控制寄存器。
Chip_DMA_Table[DMAREQ_USART0_TX].source = DMA_ADDR(uartData + length);
Chip_DMA_Table[DMAREQ_USART0_TX].dest = DMA_ADDR(&(LPC_USART->FIFOWR));
Chip_DMA_Table[DMAREQ_USART0_TX].next = DMA_ADDR(0);
Chip_DMA_Table[DMAREQ_USART0_TX].xfercfg = DMA_XFERCFG_CFGVALID | DMA_XFERCFG_SETINTA | DMA_XFERCFG_SWTRIG | DMA_XFERCFG_WIDTH_8 | DMA_XFERCFG_SRCINC_1 | DMA_XFERCFG_DSTINC_0 | DMA_XFERCFG_XFERCOUNT(length + 1);
LPC_DMA->DMACH[DMAREQ_USART0_TX].XFERCFG = Chip_DMA_Table[DMAREQ_USART0_TX].xfercfg;
这段程序在某GCC版本-O2优化选项下,生成的重排序的汇编程序。为了可读性,小编把汇编语言“翻译”成C语言,如下面所示
Chip_DMA_Table[DMAREQ_USART0_TX].xfercfg = DMA_XFERCFG_CFGVALID | DMA_XFERCFG_SETINTA | DMA_XFERCFG_SWTRIG | DMA_XFERCFG_WIDTH_8 | DMA_XFERCFG_SRCINC_1 | DMA_XFERCFG_DSTINC_0 | DMA_XFERCFG_XFERCOUNT(length + 1);
LPC_DMA->DMACH[DMAREQ_USART0_TX].XFERCFG = Chip_DMA_Table[DMAREQ_USART0_TX].xfercfg;
Chip_DMA_Table[DMAREQ_USART0_TX].source = DMA_ADDR(uartData + length);
Chip_DMA_Table[DMAREQ_USART0_TX].dest = DMA_ADDR(&(LPC_USART->FIFOWR));
Chip_DMA_Table[DMAREQ_USART0_TX].next = DMA_ADDR(0);
在上述程序中,Chip_DMA_Table[...].xfercfg与LPC_DMA->DMACH[...].XFERCFG在语句上就存在数据依赖,因此编译器不会改变这两个变量的相对访问顺序。
上述对LPC_DMA->DMACH中分量XFERCFG的赋值,意味着启动DMA操作,而其它语句是为了配置DMA操作的参数。如果把描述符中其他成员的赋值,重排序到XFERCFG的赋值之后,则显然DMA的操作不能正确地进行了。
这时,编译时指定内存屏障就可以帮助编译器理解数据访问的依赖性,保证在屏障后的数据访问不会被重新排序到屏障之前。
编译时设置内存屏障和编译器相关,GCC编译器中设置内存屏障的语句是asm volatile("" ::: "memory"); (小编也没听过关于编译时内存屏障在IAR或者KEIL的应用)。这段程序加上编译时内存屏障后变成下面这样。
Chip_DMA_Table[DMAREQ_USART0_TX].source = DMA_ADDR(uartData + length);
Chip_DMA_Table[DMAREQ_USART0_TX].dest = DMA_ADDR(&(LPC_USART->FIFOWR));
Chip_DMA_Table[DMAREQ_USART0_TX].next = DMA_ADDR(0);
Chip_DMA_Table[DMAREQ_USART0_TX].xfercfg = DMA_XFERCFG_CFGVALID | DMA_XFERCFG_SETINTA | DMA_XFERCFG_SWTRIG | DMA_XFERCFG_WIDTH_8 | DMA_XFERCFG_SRCINC_1 | DMA_XFERCFG_DSTINC_0 | DMA_XFERCFG_XFERCOUNT(length + 1);
asm volatile("" ::: "memory");
LPC_DMA->DMACH[DMAREQ_USART0_TX].XFERCFG = Chip_DMA_Table[DMAREQ_USART0_TX].xfercfg;
当然,为了避免编译时的访问内存的指令被重排序,除了考虑加入内存屏障,降低驱动程序的编译优化等级也许是更加简便的方法。当然,如果希望调整编译优化等级,也要通过检测汇编指令来进行确认。
值得提醒的时,编译时内存屏障只影响编译时的内存访问重排序,而对运行时的内存访问重排序没有作用。小编下面再讲讲运行时的内存访问重排序。
运行时的内存访问重排序
这个问题存在于Arm CPU核的设计中。
在ARMv7-M和ARMv6-M处理器中,程序的指令顺序不一定和执行顺序一致。在Arm的消息中心,官方解释了四点原因
处理器可以在不影响指令行为的前提下重新排序内存访问来改善程序效率
处理器存在多个总线接口
存储器和设备可能在不同的存储器地址分支上互联
一些存储器访问可能存在缓冲
在Arm的MPU配置中,内存访问属性可以被配置成为普通访问(Normal Access),设备访问(Device Access)和强顺序访问(Strongly-ordered access)。
内存访问属性可以通过MPU区域属性和大小寄存器(MPU Region Attribute and Size Register)中的TEX、S、C、B位设置。
当MPU没有设置时,存储器具有默认的内存访问属性,如下表所示
地址范围 | 存储器区域 | 访问类型 |
0x00000000- 0x1FFFFFFF | 程序 | 普通访问 |
0x20000000- 0x3FFFFFFF | SRAM | 普通访问 |
0x40000000- 0x5FFFFFFF | 外设 | 设备访问 (非共享) |
0x60000000- 0x9FFFFFFF | 外部RAM | 普通访问 |
0xA0000000- 0xBFFFFFFF | 外部设备 | 设备访问 (共享) |
0xC0000000- 0xDFFFFFFF | 设备访问 (非共享) | |
0xE0000000- 0xE00FFFFF | 私有外设总线 | 强顺序访问 |
0xE0100000- 0xFFFFFFFF | 厂商设备 | 设备访问 |
ARMv7-M和ARMv6-M处理器根据存储访问类型有以下规则:
若存储器访问指令A1、A2,且在程序中,A1在A2之前。当A1,A2符合下表中由符号“<”表示所述的条件时,CPU保证先执行A1指令,后执行A2指令。
由于CPU可以保证外部设备地址空间的访问顺序,对于应用开发者而言,普通访问的SRAM变量访问重排序,也就成为最需要关注的问题。
ARM提供了以下内存屏障用于内存读取或者存储的访问同步。
数据内存屏障(Data Memory Barrier) DMB
数据内存屏障可以保证程序中,在DMB之前的所有数据访问,在CPU运行时先于DMB之后的数据访问执行。数据同步屏障(Data Synchronization Barrier)DSB
数据内存屏障可以保证程序中,在DSB之前的所有数据访问,在CPU运行时先于DSB之后的数据访问执行,且所有对系统控制区的访问,都会保证在DSB之前完成,在DSB完成之前,在DSB指令后面的任何指令都不可以执行。指令同步屏障(Instruction Synchronization Barrier)ISB
ISB指令冲掉CPU流水线,程序中所有ISB之后的指令,只有在ISB执行完成后再取指。
那什么时候应该在Cortex-M系列处理器的程序中,使用内存屏障指令呢?小编这里也推荐给大家一个ARM的应用笔记,ARM Cortex™-M Programming Guide to Memory Barrier Instructions,有兴趣的同学可以参考以下。
后记
在构思本文期间,关于volatile和编译时内存访问重排序的问题,小编和周围的同事进行过热烈的讨论,这个问题在stackoverflow上也有很多讨论,读者可以自行检索。
对于访问volatile变量在编译时被重排序的问题,C标准和C++标准都没有提及,开发者普遍接受的观点是“volatile变量的编译时重排序取决于编译器”。ICCARM(IAR)/ ARMCC(KEIL)是嵌入式系统开发中常用的编译器,对于这些编译器,小编也没有听说过关于编译器重排序导致的问题。
1.2019年第1期《单片机与嵌入式系统应用》电子刊新鲜出炉!
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。