最近工作中遇到一个问题,大致描述一下:
我们SOC用的arm cortex m7内核,在设计时设计人员图方便,将SPI controller的寄存器(即原本应该是APB空间)放在了0x60000000的某一块空间(此空间arm的memory定义区间为external memory),然后同时把SPI flash的存储空间也映射在了0x60000000的这一块区间内。后者将spi flash memory映射在此区间内其实时合理的。但是将spi controller的寄存器也放在这块空间,验证人员只验证了对寄存器的访问,是可以的,此时并没有发现什么问题。
说明一下,spi controller用的synopsis的design ware IP:SSI。
SSI要求在设置一些配置寄存器前必须先disable SSI模块,所以软件先对该寄存器设置,即对使能寄存器下disable=0,然后开始对一些配置寄存器对配置设定,比如位宽,clock divider等。注意有个前提,SOC powerup之后,使能寄存器默认是enable的。!所以还必须提前disable动作。
debug软件时发现,在disable后的第一个配置寄存器的设定总是写不进去!
后来通过验证simulation发现,在CPU出来的bus上,disable寄存器的设定竟然在第一个配置寄存器设定的后面,可是软件明明时先设定的disable。所以导致第一个配置寄存器的配置软件这边始终看不到生效!!!!
至于原因,我们猜测时arm cortex-m7 cpu认为0x60000000区域是external memory区域,再加上cm7支援乱序发射双发射等等因素,导致cpu往spi controller发送是按照了memory属性,可能乱序store了。这种情况在APB总线上应该是不可能发生的,不过验证人员没有继续追究?!!待后面补吧。
这其实是CPU出来的AXI总线和CPU指令乱序有关的问题,不像是编译器乱序的问题。!
解决办法: 我在disable寄存器设定后面加了__DSB()指令就可以了。!!!即让DSB指令前面的所有指令完成后再执行后面的指令,即数据同步屏障指令!我估计__DMB()和__ISB()也是可以的。
【转】现代 CPU中指令的执行次序不一定按顺序执行,没有相关性的指令可以打乱次序执行,以充分利用 CPU的指令流水线,提高执行速度。同时,编译器也会对指令进行优化,例如,调整指令顺序来利用CPU的指令流水线。这些优化方式,大部分时候都工作良好,但是在一些比较复杂的情况可能会出现错误,例如,执行同步代码时就有可能因为优化导致同步原语之后的指令在同步原语前执行。
内存屏障和编译屏障就是用来告诉CPU和编译器停止优化的手段。编译屏障是指使用伪指令“memory”告诉编译器不能把“memory”执行前后的代码混淆在一起,这时“memory”起到了一种优化屏障的作用。内存屏障是在代码中使用一些特殊指令,如ARM中的dmb、dsb和isb指令,x86中的sfence、lfence和mfence指令。CPU遇到这些特殊指令后,要等待前面的指令执行完成才执行后面的指令。这些指令的作用就好像一道屏障把前后指令隔离开了,防止CPU把前后两段指令颠倒执行。
(1)ARM平台的内存屏障指令。
dsb:数据同步屏障指令。它的作用是等待所有前面的指令完成后再执行后面的指令。
dmb:数据内存屏障指令。它的作用是等待前面访问内存的指令完成后再执行后面访问内存的指令。
isb:指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。
(2)x86平台上的内存屏障指令。
sfence:存储屏障指令。它的作用是等待前面写内存的指令完成后再执行后面写内存的指令。
lfence:读取屏障指令。它的作用是等待前面读取内存的指令完成后再执行后面读取内存的指令。
mfence:混合屏障指令。它的作用是等待前面读写内存的指令完成后再执行后面读写内存的指令。
要精确地理解这些指令的含义,需要去查阅处理器的说明。这里只是对它们做了一点简单的介绍。下面看看Android是如何利用这些指令来实现内存屏障和编译屏障的:
1.ARM平台的函数代码
(1)编译屏障:
编译屏障即在代码前后加如下代码,编译器就不会将此区间的内存访问乱序操作
void android_compiler_barrier()
{
asm_volatile_("" : : : "memory");
}
编译屏障的实现只是使用了伪指令memory。
(2)内存屏障:
void android_memory_barrier()
{
#if ANDROID_SMP == 0
android_compiler_barrier();
#else
__asm__volatile_("dmb" : : : "memory");
#endif
}
void android_memory_store_barrier()
{
#if ANDROID_SMP == 0
android_compiler_barrier();
#else
__asm_volatile_("dmb st" : : : "memory");
#endif
}
内存屏障的函数中使用了宏ANDROID_SMP。它的值为0时表示是单CPU,这种情况下只使用编译屏障就可以了。在多CPU情况下,同时使用了内存屏障指令“dmb”和编译屏障的伪指令“memory”。函数android_memory_store_barrier()中的dmb指令还使用了选项st,它表示要等待前面所有存储内存的指令执行完后再执行后面的存储内存的指令。
2.x86平台下的函数代码
(1)编译屏障:
void android_compiler_barrier(void)
{
__asm__ __volatile__ ("" : : : "memory");
}
和ARM平台下一样,编译屏障的实现只是使用了伪指令memory。
(2)内存屏障:
#if ANDROID_SMP == 0
void android_memory_barrier(void)
{
android_compiler_barrier();
}
void android_memory_store_barrier(void)
{
android_compiler_barrier();
}
#else
void android_memory_barrier(void)
{
asm__volatile_("mfence" : : : "memory");
}
void android_memory_store_barrier(void)
{
android_compiler_barrier();
}
#endif
x86平台也一样,如果是单CPU,内存屏障的实现只使用了编译屏障。在多CPU情况下,函数android_memory_barrier()使用了CPU指令“mfence”,对读写内存的情况都进行了屏障。但是android_memory_store_barrier()函数只使用了编译屏障,这是因为Intel的CPU不对写内存的指令重新排序。所以不需要内存屏蔽指令。
真正的屏障指令的应用场景举例:
① ARM中断程序中,如:
// 中断处理函数
void INT_LED_HANDLER(void)
{
/* Clear interrupt flag.*/
INT_ClearStatusFlags(PIT, kPIT_Chnl_0, kPIT_TimerFlag);
pitIsrFlag = true;
__DSB();
}
程序通过中断信号进入中断处理函数时,首先应当清除相应的中断标志位,但有些CPU的时钟太快,快于中断使用的时钟,就会出现清除中断标志的动作还未完成,CPU就又一次重新进入同一个中断处理函数,导致死循环,__DSB() 指令的作用就是避免上述情况的发生。
__DSB()的原型是:
__STATIC_FORCEINLINE void __DSB(void)
{
__ASM volatile ("dsb 0xF":::"memory");
}
可以直接调用__DSB(),在CMSIS的头文件中有定义。上面的定义原型可知,__DSB()包括了编译屏障和指令屏障两种措施。
如果只是编译屏障,可以调用CMSIS头文件中的如下定义,即相对于__DSB()只有存memory编译关键字。
/* cmsis_armcc.h */
#ifndef __COMPILER_BARRIER
#define __COMPILER_BARRIER() __memory_changed()
#endif
等价于
/* gcc */
__asm__ __volatile__("": : :"memory")
② 多CPU系统内存访问,参考下一篇