關於DMA和它的仇家

[基礎知識]什麼叫作DMA?
DMA=Direct Memory Access。這是一種經過硬件實現的數據傳輸機制。簡單的說,就是不在CPU的參與下完成數據的傳輸。
[/基礎知識]
不太明白?我舉個簡單的例子:
好比有個數組a,我但願把這個數組中的內容傳輸到另外一個數組b中。咱們假設這兩個數組都是同樣大。好比int a[10000];int b[10000];。
那麼我能夠這樣作:
[code=c]for(int x=0;x<sizeof(a)/sizeof(int);x++){
    b[x]=a[x];
}
[/code]
循環將數組中的每一個元素進行傳遞。這是最簡單的一種方法,也是最容易理解的方法。
不過這種方法雖然簡單,效率可算不上高。若是你瞭解微機原理和彙編的話就明白了,b[x]=a[x];這句話並不是像你看到的那樣,把a[x]中的元素值賦給b[x]。
那是怎麼一個過程?
其實是這樣的:首先a[x]中的元素值賦給某CPU中的寄存器,而後再將該寄存器的值賦給b[x]。
爲何會這樣?
這是由於a和b都是在內存中的,而CPU不容許內存直接進行數據傳輸。因此在這個過程當中CPU必須參一腳當中介。
可想而知每賦值一次都要中介,效率就這麼被降下去了。
既然問題是出在CPU當中介這個地方,那麼有什麼方法能夠迴避掉這個瓶頸呢?有。那就是DMA。
DMA是一種硬件設備。這種設備的工做原理是這樣的:
——首先CPU告訴DMA設備,要有一堆數據須要傳輸,爲了效率而請它出馬。(DMA請求)
——DMA收到CPU的消息,開始準備。此時CPU把數據源地址、數據目標地址、傳輸數據量、傳輸模式等等參數告訴它。(DMA初始化)
——DMA初始化完,向CPU發送消息「借你的總線用一用,我要開始傳輸數據了!」(總線出借,DMA啓動)
——CPU收到消息後,暫時切斷本身與總線的聯繫。DMA開始傳輸數據。(DMA數據)
——DMA傳輸完數據以後,向CPU發送消息「搞定了!總線還給你。」(總線歸還)
——CPU說:「幹得好!老將出馬一個頂倆!辛苦了,你先歇着吧。」DMA設備中止。CPU該幹啥幹啥。
因爲是硬件實現的,因此DMA的速度很是快。快到什麼程度呢?在DS上,尤爲是數據量很是大的時候,相比於CPU當中介,效率可以提升一百萬倍以上。
因爲DMA的速度是如此之快,因此大量的數據傳輸,通常都要求使用DMA。
那麼,剛纔那個例子就能夠寫成:
[code=c]dmaCopy((void*)a,(void*)b,sizeof(a));
[/code]
可是DS是個很特殊的平臺,在某些狀況下,DMA不適用。有些內存區域,DMA是沒法訪問的。那就是BIOS、TCM和Cache。

[基礎知識]BIOS是一塊被硬件保護的內存區域。這塊區域正常狀況下是「讀寫保護」。也就是說,採用正常的方法是沒法訪問這塊內存的。天然,DMA也沒法訪問。直接讀取BIOS,讀出來的全是隨機的數據。
那麼,我想DUMP BIOS,該怎麼作呢?
這就須要一些技巧了。如今我先不說,到教程的最後我再把這個坑給填了。
[/基礎知識]
[基礎知識]TCM=Tightly Coupled Memory。這是一種高速緩存,聽說是被直接集成在CPU芯片中。DS有兩種TCM,分別是ITCM(Instruction TCM)和DTCM(Data TCM)。不用解釋大家也能知道這兩種TCM是幹啥用的。
[/基礎知識]
因爲是高速緩存,因此這兩塊內存區域被當作特殊的用途。好比某些對時間要求很是嚴格的代碼,就能夠被放到ITCM中執行。這能夠有效地提升運行速度。某些須要頻繁存取的數據,也能夠放到DTCM中以節省存取時間。
怎麼樣把代碼放到ITCM中?有兩種方法。一種是使用gcc特有的「屬性標籤」,將指定代碼賦予「ITCM」屬性,此時該代碼會被載入ITCM中執行。還有一種方法是直接將.c源文件改爲.itcm.c,此時源文件會被直接編譯成在ITCM中運行的目標文件。
而DTCM就方便得多了。雖然兩個TCM都是可映射的,也就是說,它們的地址並不是固定,可是NDSLIB的lnkscript將這兩塊TCM映射到了0x01000000和0x0B000000上。既然已經有了固定地址,那麼就能夠很輕鬆地訪問了。不過,正如剛纔所說的,這兩塊內存空間都是有特殊用途的,因此不建議直接訪問。相比於ITCM來講,DTCM更加劇要。由於在這塊內存中,存在着一個很是重要的對象——棧。
「棧」這種東西我也不詳細解釋了。局部變量和函數調用的參數,就是靠棧進行傳遞的。
因爲DMA沒法訪問TCM,因此也就沒法訪問棧。又因爲局部變量是被開闢到棧中,因此DMA也沒法對局部變量進行傳遞。舉個例子:
我要往主引擎的標準調色盤中填充隨機顏色。
下面的代碼就是錯誤的:
[code=c]void fillRandomColorToMainPalette(){
    u16 tmpPalette[256];
    dmaCopy((void*)tmpPalette,(void*)BG_PALETTE,sizeof(tmpPalette));
}
[/code]
緣由很簡單,tmpPalette中的數據雖然是隨機的,但這個數組是局部變量,被開闢在棧中,DMA沒法訪問。
下面則是正確方法:
[code=c]void fillRandomColorToMainPalette(){
    u16 tmpPalette[256];
    memcpy((void*)BG_PALETTE,(void*)tmpPalette,sizeof(tmpPalette));
}
[/code]
memcpy不是須要CPU參與嗎?那豈不是很慢?
是的。很慢,相比於DMA來講慢多了。不過,目前咱們只能用它。教程的最後我將教你一種又快又安全的方法。

[基礎知識]什麼是Cache?
衆所周知CPU的速度很是快。當CPU訪問外設的時候,有些外設速度比較慢,響應CPU比較遲鈍。此時CPU要麼等外設響應,要麼繼續幹它的活等外設的中斷信號。
可是有些外設是沒有中斷的。此時CPU就必須等了。最典型的例子就是內存。
當CPU訪問內存的時候,並不是像你想象的那樣,CPU馬上就能訪問到它想訪問的內存空間,而是有一個「WaitState」的過程。想一想看吧,每訪問一次內存都要等上幾個機器週期,這可不是個好事~~~尤爲是,這個「幾」可不是簡單的一位數,有些時候甚至能達到3位數。
那麼這個問題又該怎麼解決呢?那就是Cache了。
Cache是集成在CPU內部的極高速的緩存。注意關鍵詞「極高速」。通常來講,它的訪問速度幾乎能夠媲美CPU。這就意味着,CPU在訪問Cache的時候幾乎不會浪費多少時間。不過,速度的提高是用容量做爲代價的。Cache的容量很小。在DS中,數據緩衝(Data Cache,簡寫爲DC)只有4K,指令緩衝(Instruction Cache,簡寫爲IC)只有8K。
[/基礎知識]
那麼,咱們把經常使用的數據放到Cache中,CPU在訪問的時候直接訪問Cache就好了,不用耗費時間去訪問內存了。
事實上CPU就是這麼作的。在讀內存的時候,CPU首先讀Cache,看看有沒有它想要的數據的「副本」,有的話那就太好了,直接拿過去用。沒有的話就只好費點功夫去讀內存了。而在寫內存的時候,CPU直接寫到Cache中,而非直接寫到內存中。
哪尼?
確實是這樣子的。當Cache寫滿了以後,此時纔將Cache中的數據更新到內存,同時清空Cache。就像寄信同樣,全部的信件會首先攢到郵局,到達必定數量以後纔會送出去。
不過這又出現一個問題:假如Cache中有某個內存數據的「副本」,那麼CPU在讀該內存的時候就會直接使用該副本而不用去讀內存。那萬一內存中的數據被改寫,此時CPU再讀該內存,讀出來的豈不是那個舊的副本而不是最新的內存數據?一樣,假如我想DMA一些數據,誰能保證此時內存中的數據就是最新的數據?
某人:那啥,我直接讀寫Cache得了。鳥內存真它奶奶的麻煩!
很惋惜,Cache是徹底的黑箱。你不知道它的地址。你也沒法直接訪問它。
那該怎麼辦呢?幸虧NDSLIB爲咱們提供了一些函數:
[code=c]// 將整個Data Cache更新到內存
void DC_FlushAll()
// 將Data Cache中指定地址指定大小的區域更新到內存
void DC_FlushRange(const void *base, u32 size)

// 清空整個Data Cache
void DC_InvalidateAll()
// 清空Data Cache中指定地址指定大小的區域
void DC_InvalidateRange(const void *base, u32 size)

// 清空整個Instruction Cache
void IC_InvalidateAll()
// 清空Instruction Cache中指定地址指定大小的區域 
void IC_InvalidateRange(const void *base, u32 size)
[/code]
那麼,何時使用這些函數呢?
在DMA以前,我須要保證數據源內存中的數據是最新的。因此此時須要Flush,從而使DC中的副本可以更新到內存中。
在DMA以後,我須要保證DC中的副本和內存中的數據是相同的。可是NDSLIB沒有更新DC的函數,因此沒辦法,咱們只能把DC中的副本殺掉。此時若是CPU訪問內存,因爲DC中沒有副本,因此就只能直接從內存訪問並將訪問到的值做爲DC中副本了。因此此時須要Invalidate。
至因而用All仍是Range,很明顯,All的話確定最保險,但確定也會更花時間。因此若是你有把握,那就用Range以節省時間;沒把握就用All求穩妥。

注意,模擬器在模擬Cache上至關不完善。根據個人觀察,三大著名模擬器——NO$GBA、desmume和ideas,都沒法正確模擬Cache。因此若是你發如今模擬器上圖像正常而到了真機上出現破碎、混亂、顏色異常等等問題,設法把你的DMA函數先後加上DC_Flush...(...);和DC_Invalidate...(...);。

[填坑]正如我所說的,DMA很是之快。通常狀況下的數據傳輸,靠它沒問題。不過有些場合DMA沒法訪問,我又想要快,那該怎麼辦呢?有請swiCopy和swiFastCopy!
[code=c]#define COPY_MODE_HWORD  (0)
#define COPY_MODE_WORD  (1<<26)
#define COPY_MODE_COPY  (0)
#define COPY_MODE_FILL  (1<<24)
void swiCopy(const void * source, void * dest, int flags);
void swiFastCopy(const void * source, void * dest, int flags);
[/code]
這兩個函數是所謂的「BIOS軟中斷」,又稱「系統調用」。你能夠把他當作是GBA/DS特有的函數。
這兩個函數很是神奇,在GBA中,它們比DMA更快!不過很惋惜,在DS中則一點都不快,甚至比memcpy還慢。
這兩個函數沒有區域限制。不論是BIOS仍是內存,不論是ITCM仍是DTCM,都能直接訪問。
如今咱們來改寫一下那個隨機調色盤的例子:
[code=c]void fillRandomColorToMainPalette(){
    u16 tmpPalette[256];
    swiCopy((void*)tmpPalette,(void*)BG_PALETTE,COPY_MODE_WORD|COPY_MODE_COPY|(sizeof(tmpPalette)>>2));
}
[/code]
注意最後一個參數。它的單位是WORD,即4字節。所以數據大小須要除以4以轉換成WORD。
swiFastCopy則比swiCopy更快。你如果追求更高的速度也能夠換成swiFastCopy。不過注意,這個函數只能傳輸半字寬度,也就是2字節的數據,所以不能使用COPY_MODE_WORD這個模式。可是傳輸數據大小依然以WORD爲單位。
[/填坑]
最後咱們把坑填了吧。這是DUMP ARM9的BIOS的函數:
[code=c]
#define BIOS_ADDRESS 0xFFFF0000
#define BIOS_SIZE 32768
void dumpARM9BIOS(){
    void* tmpBuffer=calloc(BIOS_SIZE,1);
    swiCopy((void*)BIOS_ADDRESS,(void*)tmpBuffer,COPY_MODE_WORD|COPY_MODE_COPY|(BIOS_SIZE>>2));
    DC_FlushAll();
    FILE* f=fopen("arm9.bios","wb+");
    if(f!=NULL)fwrite(tmpBuffer,BIOS_SIZE,1,f);
    fclose(f);
    free(tmpBuffer);
}
[/code]ios

相關文章
相關標籤/搜索