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就已經跑去讀,那麼讀到的並不是期望的資料。該過程如圖所示:
另外,做過較為小型的對效能要求不太高的嵌入式裸板程式(例如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搬完後,你再訪問記憶體裡的內容很可能是個很奇怪的數字。