1 基本概念
在嵌入式软件开发中,经常会碰到说某块内存是cache的,还是non-cache的,它们究竟是什么意思?分别用在什么场景?non-cache和cache的内存区域怎么配置?这篇博文将会围绕这几个问题展开讨论。
Cache,就是一种缓存机制,它位于CPU和DDR之间,为CPU和DDR之间的读写提供一段内存缓冲区。cache一般是SRAM,它采用了和制作CPU相同的半导体工艺,它的价格比DDR要高,但读写速度要比DDR快不少。例如CPU要执行DDR里的指令,可以一次性的读一块区域的指令到cache里,下次就可以直接从cache里获取指令,而不用反复的去访问速度较慢的DDR。又例如,CPU要写一块数据到DDR里,它可以将数据快速地写到cache里,然后手动执行一条刷新cache的指令就可以将这片数据都更新到DDR里,或者干脆就不刷新,待cache到合适的时候,自己再将内容flush到DDR里。总之一句话,cache的存在意义就是拉近CPU和DDR直接的性能差异,提高整个系统性能。
2 哪些情况不能用cache
在大部分时候,cache是个很好的帮手,我们需要它。但也有例外,考虑下面几个场景
case 1 CPU读取外设的内存数据,如果外设的数据本身会变,如网卡接收到外部数据,那么CPU如果连续2次读外设的操作相差时间很短,而且访问的是同样的地址,上次的内存数据还存在于cache当中,那么CPU第二次读取的可能还是第一次缓存在cache里数据。
case 2 CPU往外设写数据,如向串口控制器的内存空间写数据,如果CPU第1次写的数据还存在于cache当中,第2次又往同样的地址写数据,CPU可能就只更新了一下cache,由cache输出到串口的只有第2次的内容,第1次写的数据就丢失了。
case 3 在嵌入式开发环境中,经常需要在PC端使用调试工具来通过直接查看内存的方式以确定某些事件的发生,如果定义一个全局变量来记录中断计数或者task循环次数等,这个变量如果定义为cache的,你会发现有时候系统明明是正常运行的,但是这个全局变量很长时间都不动一下。其实它的累加效果在cache里,因为没有人引用该变量,而长时间不会flush到DDR里
case 4 考虑双cpu的运行环境(不是双核)。cpu1和cpu2共享一块ddr,它们都能访问,这块共享内存用于处理器之间的通信。cpu1在写完数据到后立刻给cpu2一个中断信号,通知cpu2去读这块内存,如果用cache的方法,cpu1可能把更新的内容只写到cache里,还没有被换出到ddr里,cpu2就已经跑去读,那么读到的并不是期望的数据。该过程如图所示:
![](https://img-my.csdn.net/uploads/201608/02/1470134591_7195.png)
另外,做过较为小型的对性能要求不太高的嵌入式裸板程序(例如bootloader,板级外设功能验证之类)的朋友都知道,cache经常会帮倒忙,所以他们往往会直接把CPU的cache功能关掉,损失一点性能以确保CPU写入和读出的数据与操作的外设完全一致。
这里我们可以稍微总结一下:对于单CPU,不操作外设,只对DDR的读写,可以放心的使用cache。对于其它情况,有两种办法:
1 手动更新cache,这需要对外设的机制较为了解,且要找到合适的时机刷新(将cache里的数据flush到内存里)或无效(Invalidate,将cache里的内容清掉,下次再读取的时候需要去DDR里读最新的内容)
2 将内存设置为non-cache的,更准确的说是non-cacheable的
3 怎么设置内存为non-cacheable?
不同的处理器平台对于non-cacheable的处理办法也是不一样的,在高级CPU里,一般在运行中,动态地采用页表的方式来标记某些内存是否是non-cacheable的,例如Linux内核里的
有个常用的函数叫ioremap,在访问外设的时候经常会用到,它的作用是映射外设的物理地址到虚拟地址空间给内核驱动程序使用,在映射时,会将寄存器地址页表配置为non-cacheable的,数据直接从外设的地址空间读写,保持了数据的一致性。
在较为低级的CPU(如ARM Cortex-A5,MIPS R3000.接近于MCU)里,一般是采用编译链接时预设的方法来区分cache区域和non-cache区域,在链接脚本或者scatter file里定义不同的section,将需要设为non-cacheable的内存块放置到特定的section里。CPU在启动时会读取这些配置文件,在启动代码里对不同的section配置MMU或MPU,对non-cache的section映射到CPU认识的特拟地址。此后,CPU再访问内存的时候,经过MMU或者MPU,就会知道这块内存是cacheable还是non-cacheable的了
在MIPS里,程序地址空间被分为了kseg0,kseg1等区域,其中kseg0是0x80000000~0x9FFFFFFF,它是非映射的、cached;kesg1是0xA0000000~0xBFFFFFFF,它是非映射的,uncached,他们指向的物理地址空间是相同的,也就是说,0x82001234和0xA2001234指向的物理地址是相同的,但是MIPS对它们的访问方式不同,当取指后CPU得知要访问的是kseg0的空间,会想去cache寻找目标内存,若找不到,才会去物理地址寻找,而如果要访问的地址空间是kseg1内,则CPU会绕过cache,直接去物理地址里进行读和写。
4 non-cacheable区域的大小有限制吗?
答案是否定的,只要你乐意,甚至可以把几乎整个DDR都设置为non-cacheable的,但是这样做付出的代价也是巨大的,你等于放弃了CPU厂家精心设计的cache机制,在每次读写数据都要去访问比CPU速度慢得多的DDR,整个系统速度会被严重拖慢。楼主做过一个测试,在同等条件下,写cache的内存速度大约比non-cache的内存快一倍的!
5 应用案例
假设某个嵌入式应用需要将一个4K的内存拷贝到另一个地址去,源和目的映射的物理地址都在DDR里。很自然的,我们会想到勤劳的搬运工——DMA,让DMA去做这个枯燥而费时的拷贝工作,让CPU忙别的去。经常使用DMA的同学都知道,它有标准的3个步骤:
1) 对源地址进行cache flush,防止源地址里的内容已经在cache里更新,而DDR里还是过时的内容,DMA可不知道何为Cache
2) 对目标地址进行cache invalidate,因为这个目标地址里的内容马上就要被覆盖了,cache里如果有它的内容,就是过时的了
3) 设置源、目的地址和数据长度,启动DMA
是不是挺麻烦的? 如果源地址和目的地址都是以全局数据的形势预设置好的话,完全可以将它们定义在non-cacheable的段内,这样第1)步和第2)步就都不再需要了。请注意,源和目的地址都需要定义到non-cache的区域,少一个的话,你都很可能得不到正确的结果,因为cache的强随机性,DMA搬完后,你再访问内存里的内容很可能是个很奇怪的数字。