2010年1月26日 星期二

NUC501-實作虛擬記憶體(ROM篇)

前次提到在NUC501實作虛擬記憶體的可行性,愈想愈覺得可以試試看,但前提是NUC501的ABORT例外沒有被閹割掉才行,所以想先做個實驗看看NUC501對於ABORT有沒有反應。

從技術資料得知,ARM的ABORT例外分為二個,一個是指令預取例外,當程式碼(R15)執行的區域不存在時,會引發指令預取例外,另一個則是資料取得例外,當程式嘗試去存取不存在的記憶體時,會引發資料取得例外。所以最簡單的測試方法,是先嘗試資料取得例外,就能知道NUC501的ABORT是否能用。

不過沒ICE的情況下,不容易知道是不是有中斷進來,還好開發板上已經裝配了8顆LED,所以可以透過LED的亮暗情形來得知。從範例程式中複製了LED的初始化設定,讓LED恆亮,然後修改crt0裡的DataAbortHandler,讓它在收到中斷時關閉LED。最後在測試程式中寫一行:
*((unsigned long *)0x10000000)=0;
編譯好後放到開發板上執行,哇!沒想到真的會動耶~~看到LED熄滅,害我著實的高興了好一陣子。既然NUC501可以支援ABORT(看來連SRAM的MMU機制都是預先規劃好的!),那麼不僅是ROM,連RAM都可以做虛擬記憶體耶(當時還沒想到同時實現的困難點)~~衝著好的開始是成功的一半,便興沖沖的規劃了要把SRAM分一半給虛擬記憶體做換頁,其中的四頁給ROM,另外四頁給RAM,規劃把ROM放在0x10000000的位址,而RAM預定放在0x08000000的位址之後,便開始深入了解ARM7TDMI的ABORT機制。

指令預取的部份還算容易,以NUC501的架構為例,在收到例外後,把LR的位址減去4,做2048的對齊之後,就是需要換頁的位址。由於ROM不需要回寫,所以只要找到空閒的頁面,把正確的資料複製進去,再把頁面mapping到應出現的位址即可。不過由於我不懂頁面切換演算法,所以就簡單的直接以位址線來決定使用哪個頁面,如位址0x10000000、0x10002000、0x10004000會使用相同的頁面,以此類推。資料例外的部份比較複雜,就想先不去管它,反正還沒有要實作RAM的部份。把crt0做必要修改,並寫好PrefetchAbortHandler後,修改link script把.text放到0x10000000,然後燒到SpiROM上執行,我遭遇到悲慘的當機狀況~~"

在反覆的驗證指令預取是否有問題,我突然想到我犯了一個嚴重的錯誤,那就是指令預取與資料例外是相輔相成的!由於ARM大量使用參考讀取的指令,那麼執行程式時,除了指令預取外,資料例外也會出現!這代表我得先寫好資料例外之後,才能看到成果,害我著實的沮喪好一陣子。上網搜尋看看有沒有簡單的解決方法,大部分的網站都說資料例外要寫一個小型的disassembler,然後去看是哪個指令引發例外,然後修正它!洋洋灑灑說得到容易,我會寫的話,啊事情不就好辦很多!

該來的逃不掉,為了實現虛擬記憶體,還是得做資料例外的處理了。打開ARM7TDMI的技術資料,開始認真的研究,關於資料例外是這麼說的:
If a data abort occurs, the action taken depends on the instruction type:
  1. Single data transfer instructions (LDR, STR) write back modified base registers: the Abort handler must be aware of this.
  2. The swap instruction (SWP) is aborted as though it had not been executed.
  3. Block data transfer instructions (LDM, STM) complete. If write-back is set, the base is updated. If the instruction would have overwritten the base with data (ie it has the base in the transfer list), the overwriting is prevented. All register overwriting is prevented after an abort is indicated, which means in particular that R15 (always the last register to be transferred) is preserved in an aborted LDM instruction.
簡單來說,就是
  1. 處理LDR/STR時要注意write back會破壞基底暫存器的問題,也就是說,收到例外後必須找到並還原基底暫存器的原始值,不然就等著當機吧(原廠沒這麼說啦,我自己加的啦~~")
  2. SWP(這我還是第一次知道有這個指令),在資料例外的時候尚未執行。
  3. 區塊搬移指令LDM/STM,寫一堆文言文看不懂,只知道好像暫存器列表中的暫存器不會被破壞等等的,只好做實驗才知道怎麼回事了。
看來真的得寫disassembler,不然就無法知道那個暫存器是基底暫存器,被加了多少位移,要把一個一個暫存器的值去mapping嗎?別開玩笑了。除此之外,還必須檢查系統例外前的工作模式,所以不僅要處理ARM指令,還有Thumb指令也要做disassembler!為了避免弄到昏到,決定先從ARM指令開始做,到最後Thumb不能做的話,大不了不跑16-bit模式就是了(太天真了!)。便開始跟指令預取一樣,用組合語言在crt0寫了起來。

結果愈寫愈不對,這樣我要怎麼debug咧?買ICE大概無望,我也不想自己出錢,唯一有的就是COM埠輸出可以用,但程式不能在RAM跑要怎麼知道處理正不正確呢?這讓我對要不要繼續弄下去猶豫了許久,如果最後成果高不成低不就,用起來限制重重的話,還不如不要弄了。最後終於在回家的路上想到了解決的辦法!

方法是:
  1. 首先在crt0內把ABORT的堆疊加大,讓系統在呼叫DrvSIO_printf有足夠的記憶體不致於當機,也能讓ABORT有足夠的空間保存所有的暫存器,這樣我就可以用C來寫資料例外了!不僅除錯方便許多,也不用自己做最佳化(GCC的ARM最佳化做得不錯)。
  2. 接下來把程式放在SpiROM上執行(這時我真感謝有SpiROM呀!),在非必要時不要呼叫DrvSIO_printf以免發生重入。
  3. 最後要解決程式僅在SpiROM上執行,而不會跑去不存在位址的問題。其實只是我多慮了,你只能從LR-8的位址取得發生例外時的指令碼而已,實際需要發生換頁的頁面,必須透過自己寫disassembler來解決。所以解決方法,就是用GCC的inline assembly自己寫一定會發生例外的指令就好啦,這樣還可以控制要測試那個指令呢!
確定了debug方法以後,便開始實作,流程是這樣的:

  1. 負責資料例外處理的函式必須放在固定RAM中,所以透過修改link script把程式夾在startup與main之間的RAM區域。
  2. 直接修改crt0的DataAbort進入點,讓它直接跳到自己寫的處理函式中。
  3. 處理函式一開始先用inline assembly把所有暫存器(R0-R12,LR,SPSR)放到堆疊中。由於已經固定堆疊的位址,所以在C中可以透過固定位址來存取原始暫存器的值。
  4. 檢查SPSR中的T旗標,如果是從Thumb模式來的,就直接當機不理它(哈哈)。
  5. 如果是ARM模式,則從LR-8的位址取得指令碼,然後判斷指令形式,舉凡LDR(B)/STR(B)、LDRH/STRH/LDRSB/STRSH、LDM/STM、SWP都在處理之列。其他與NUC501架構無關的指令(如LDC/STC),就不用處裡。
  6. 處理時要注意ARM的先加後存且回寫以及先存後加,這二者都會破壞基底暫存器,必須修正堆疊內的基底暫存器,這樣在重新執行該指令時才不會發生錯誤。
  7. LDM/STM必須處理跨頁面的問題,所以很有可能一次要mapping二個頁面。
  8. 依據基底位址對齊2048,並計算好使用的頁面後,便從SpiROM複製資料進來,並將該頁面mapping到應存在的位址。
  9. 從堆疊復原暫存器(R0-R12,LR, SPSR不要),調整LR,讓CPU重新執行引發例外的指令。

經過幾天的努力,從一開始不斷的當機,到連Thumb模式也做了(後來發現DevKitPro GCC的程式庫有些是Thumb code的,最終還是無法避開),終於讓我看到程式能在0x10000000的位址跑起來!而且真的很快!但興奮感在我打開了GCC的最佳化以後很快就消失了。首先是DrvSIO_printf印字時變成老牛推車,在加了除錯用的程式碼後,會在不定位址當機,究竟是什麼問題呢?

為了找到問題點,又透過LED來幫忙(在RAM跑了以後便無法使用DrvSIO_printf了,以防止例外重入),分別在指令預取與資料例外中加入讓LED閃爍的功能,不過在努力修改除錯以後,始終找不到問題點,我幾乎快要放棄了,看來沒有ICE終究是困難呀!放棄好了。

失望之餘,卻又在回家的路上想到了問題的答案!這不就是作業系統上的「死結」嗎?死結發生的原因,是執行間接資料讀取的指令碼時,發生資料例外,而資料例外處理時,意外的又把指令所在的頁面給換掉了(二者使用同一個頁面),造成例外返回時的程式記憶體不存在,又引發指令預取例外,然後不停的循環,這在可用頁面特別少的情況下很容易發生,完全都要歸功於愚蠢的我設計了那個爛換頁程式所造成的啊!

為了確定問題是死結造成的,用DrvSYS_SetCPUClock把CPU降到18Mhz執行,使LED閃爍更明顯。果然發生死結時LED閃爍的特別厲害,永遠也跑不完,看了這情況真是又好氣又好笑。不過問題還是要解決,一是弄個頁面演算法,讓死結發生的機率降低,這有點困難。二是我另外想到的偷懶方法,從未來要處理RAM換頁的頁面借二個頁面,讓資料例外與指令預取分別使用不同的頁面區域,二者不互搶頁面,問題就解決啦!即使執行時資料頁面切換,指令好死不死剛好也執行到資料頁面上的程式碼,也不會引發預取例外(因為頁面存在),直到資料頁面切換掉被程式用的頁面時,會引發預取例外讓指令用回自己的頁面(效能損失應該只有一點點),反之亦然。

終於,NUC501的虛擬記憶體實作算是完成,說「算是」是因為不知道哪裡還有隱藏的錯誤。不過我知道的是,這一切都很值得,光是在18Mhz的速度下,輸出訊息的速度,就像是386時代把VGA BIOS放到Shadow RAM一樣,跑DIR就像飛的(以前在學校時,同學最喜歡跑DIR訊息看電腦夠不夠快)!

ROM虛擬記憶體完成了,也該實驗RAM的虛擬記憶體了。但想想在實作上有困難,主要是因為想把RAM的SWAP放在SD卡上,而檔案系統有不能重入的問題在,所以不能從例外中呼叫。就算能呼叫也勢必引發ROM頁面切換,使得系統除錯更複雜。看來這個問題還得好好想想才行。

相關文章
NUC501-實作虛擬 記憶體(RAM篇)
ROM虛擬記憶體改進篇

2010年1月20日 星期三

NUC501初探

沒想到過了這麼久,大金還是對GPS的應用念念不忘!之前有試作一台GPS軌跡記錄器,用22Mhz的標準8051、GBA專案剩下的半硬體半軟體的SD介面晶片、國產便宜的GPS接收器、二顆LED,以及32K RAM及ROM組成,完全以便宜為考量的機器。

為了能在這顆效能貧脊的8051(由於指令需12 clock,效能最快只有1.83MIPS而已),順暢的執行GPS接收、寫入SD卡,花了許久的時間改進程式碼的效率,透過檢查編譯器(SDCC)所編出的組合語言,找出能讓編譯器產出最佳的程式寫法。無法改進的部份,就直接用組合語言來加速!導致專案大半時間都耗在最佳化上(很愚蠢吧),最後雖然能夠順暢的執行所有工作,但受到半硬半軟SD介面的限制,儲存的資料偶而會有錯誤的問題!8051剩餘效能也無法進行複雜的資料分析作業。此外還有定位慢、燈號意思不易記(只有二顆LED呀,是要怎麼閃才容易記?)等問題。最重要的,就是沒有技術去開發電腦端的整合方案,最後這個專案淪為練功用,唯一受益的,就是讓之後用到8051的專案,比以前更容易搾出效能啦!

這次又因為公司的大方向還沒出來(哪次有出來?),需要多方面發展才能提供給客戶選擇。結果大金又提GPS的東西,就跟大金明說了,裝置方面應該不是問題,最大的難題在電腦端的應用,需要花很多時間去搞懂一些規格,才能寫得出像樣的應用程式,大金也說了可以跟其他廠商合作(雖然每次都這麼說,最後還不是拿些理由要我自己做...)。既然如此,就順便進讒言說想換個高階一點的CPU,不要每次都用8051。另外,裝置上也要有黑白的(彩色更讚)液晶螢幕,才像目前市面上在賣的軌跡記錄器嘛!是不是?!說不定之後還能以此架構為基底,發展出不同的應用呢!

哀求了很久,終於大金挖到Nuvoton的NUC501來作為系統的心臟!這是一顆ARM7TDMI架構的CPU,與GBA使用的相同,並不陌生,運作頻率最快可以達到108Mhz。更神奇的是,這顆CPU的報價竟然只要美金1元左右,太有競爭力了!本以為大金能同意用高效能8-bit晶片(如AVR),或16-bit的CPU就謝天謝地了。想想自從開始寫裝置系統以來,除GBA外,還沒真的在專案上使用32-bit的CPU耶~~

興奮之餘,也稍微研究了一下NUC501的規格。啥米!RAM竟然只有32KB,程式則是放在SpiROM上,且沒有直接的擴充介面。也就是說,要嘛就把程式全部放在RAM裡面,這樣可以發揮108Mhz的最大威力,但是只能寫小程式。不然就要用NUC501的特異功能,讓程式直接在SpiROM上執行,但這樣卻會受限於SPI的頻寬而折損效能。我只能說為了降低售價,真是委屈這顆ARM7TDMI的核心了。雖然有跟大金抱怨過擴充的問題,不過大金還是用一貫的藉口「成本考量」來打發了,再加上NUC501上的週邊可以說是應有盡有(COM埠、I2C、SD等),為了避免最後大金反悔,又打回8-bit的時代,只好努力用看看了。

雖經歷一番波折才拿到開發板,裡面卻沒有開發工具,大金也說ICE太貴不肯買。還好開發板有附GCC版本的程式庫原始程式,也有makefile可以參考,所以只要自己生一套GCC的開發系統,然後把開發板的程式庫重新編譯就行了。除錯方面,程式庫已經寫好方便的RS-232訊息輸出,可以稍微彌補沒有ICE的缺憾(不過有ICE才能比較快了解硬體運作的細節呀~~")。在網路上搜尋了一下,這顆晶片還沒有什麼應用實例出來,也沒有直接可以用的GCC,還好NUC501的架構與GBA一樣,讓我想到以前寫GBA時候用的DevKitPro這套開發系統,裡面就有可以編譯ARM code的GCC,或許可以拿來用哦!

在修改範例程式的makefile,並嘗試make後,雖然有warning訊息,但還是編譯成功了!把binary code燒到開發板的SpiROM上,並設定SpiROM執行後,哇!看到訊息了,還好可以執行,不然就要傷腦筋了,呵呵。接下來的工作就是整合開發環境了。

由於懶惰寫那個麻煩的makefile,在GBA時代是借用DevC++,這套IDE軟體是整合MINGW版的GCC,編譯前會自動生成簡單的makefile,並呼叫make來編譯,也可以另外指定編譯器。雖然很方便,可是DevC++主要是開發Windows的程式,所以產生的makefile自然與ARM的編譯方式有些許差異,所以需要寫個front-end來解決DevKitPro與DevC++本質上不相容的問題。

為了這個問題,以前是寫個叫做ACC(Advanced C Compiler)的front-end解決,將DevC++執行make時所傳來的編譯參數加料,補上ARM編譯所需的-mcpu -march等,然後再把加料過的參數丟給ARM的GCC去執行。除此之外,link時期還要偷偷的補上DevC++沒做的步驟,如objcopy等。所以到目前為止,已經用這個技倆讓DevC++相容Dark Fader DevKitAdv (GBA)、DevKitPro (GBA/NDS)、SDCC (8051)以及CC65 (VT1682)了,沒想到懶惰的怨念,竟然間接擴充DevC++能支援的編譯器,哈哈。還好DevKitPro原本就在支援之列,所以ACC的修改工作還算容易,在編譯好開發板附的程式庫,參考範例程式製作一個合用的crt0,整合好開發環境,就可以開始寫程式囉!

弄完IDE後稍微試了一下NUC501的效能,在SpiROM上面執行程式如預期真的有點慢,雖然我認同Nuvoton在節省產品成本的努力,而且這介面能很神奇的讓CPU核心等待SPI序列轉並列,然後才執行轉好的程式碼,但程式不是循序執行的,這使得SpiROM的缺點曝露出來。

基於SpiROM必須先下序列位址,然後才能序列讀取資料,整合成32-bit的資料後交給CPU執行,所以當程式亂序執行時,便需要經常重新執行序列位址輸出,讀取序列資料交給CPU的動作。如果要執行的指令在下一個位址還好,不是的話又得重新執行,CPU才能拿到要執行的資料。這一來一往就會浪費不少時間週期在等待,遇到經常性間接參考讀取資料的指令(這可是ARM的強項)就會慢到不行,即使SpiROM已經跑72Mhz的速度也無法即時餵飽CPU。所以應用指南有說明SpiROM可以用在「不Critical」的程式上,哇!我不想再做最佳化的苦力了!

不過NUC501有個簡單的MMU機能,可以把內部的SRAM分割成16個2K區塊,並能將區塊隨意的放置在512MB (0x00000000~0x1FFFFFFF)的位址空間上,就某些程度而言,或許可以利用這個MMU及ARM7TDMI的ABORT例外,實作虛擬記憶體來解決SpiROM頻寬不足的問題。目前想到的方法是在512MB區段找個空間當ROM區,當CPU執行到此區域時,會因記憶體不存在而引發ABORT例外,這時就從SpiROM上複製該空間的資料到RAM上,並將該RAM mapping到要執行的位址,如此程式就能繼續執行。如果使用這個方法執行程式,SpiROM造成的效能衝擊就可大幅降低,程式可以執行的更快、也更省電!不過目前也只是想想,還需要做些實驗才知道這個方法是否有用了。

一年又過去了...

小時候不這麼覺得,但長大後還真的能體會「時光飛逝,歲月如梭」這句話的涵意呀。

回顧本站去年的文章產量比起前年要多一倍,而且沒有「驟緩更新的blog」這種無意義的續集,算是不錯啦,期望今年能多寫一些有趣的東西出來充實一下版面囉。

本來這篇應該在12/31來寫的,結果那天老闆提早下班,沒來得及寫。之後又忙東忙西的一直拖到現在。本想大書特書的檢討去年,不過人應該往前看啦,過去的就讓他過去吧(喂~~)。

最後,因為某人的懶惰,這篇也跟「驟緩更新的blog」一樣,沒東西啦~~