顯示具有 工作向 標籤的文章。 顯示所有文章
顯示具有 工作向 標籤的文章。 顯示所有文章

2010年8月27日 星期五

NUC501-虛擬記憶體問題的最終回

繼上回出現RAM虛擬記憶體效能差勁,卻不知原因為何的謎團後,經過詳細的(?好啦!我承認是亂槍打鳥的)檢查、抽絲剝繭,測試各相關軟體元件的執行效能,造成效能瓶頸的犯人終於找到了!原來犯人就是「帕雷拖法則」!

根據維基百科記載,帕雷拖法則是:

帕雷托法則(Pareto principle),也稱為80/20法則,此法則指在眾多現象中,80%的結果取決於20%的原因...在電腦科學裡,帕雷托法則可藉由觀察80%的資源是由20%所操作使用,來最佳化資源。在軟體工程上,常有接近90%的電腦程式執行次數花費在10%的程式原始碼執行。

而在這個專案中,效能低落的發生與程式撰寫方式有很大的關係。由於專案開發的時候,常喜歡將使用到的功能,寫成群組型態的功能區塊。例如有專門處理LCD顯示的gfx系統、處理文字顯示的font系統,以及處理檔案系統的APSLib等等。而各功能區塊又會進行細分,有時會製造出許多單一功能的小函式。

之後為了避免上層程式執行功能時,都得要呼一堆小函式,所以又建立easy系列的函式,比方說EasyOpenFile之類的,這樣想要很輕鬆愉快的寫上層程式,就用easy系列,想要easy系列之外的低階控制就另外自己呼小函式。當同樣的低階控制程式碼多了起來,就又會為它增加了一個easy函式。上層程式寫好以後,它的上上層程式大多會形成任務分派形式的程式碼(dispatcher),因此完成版的程式碼很有大腸包小腸的Fu。大金每次看我的程式,都說我的主程式看起來簡單到不行(因為只是任務分派的迴圈而已),可是副程式卻呼來呼去複雜到死。

不過這種寫法是以前教科書上的建議(相信有很多程式設計師也是這麼做),這麼多年下來執行也都沒什麼大問題呀!為何這次會踢到鐵板呢?

主因是:這種程式寫法不適用在目前所設計的虛擬記憶體架構!

比方說,執行主程式時,會因為主程式本身是任務分派的形式,又因底下程式細分的關係,造成虛擬記憶體得從各個不同的區塊載入這些細分的程式,程式層數愈深,頁面切換的次數愈多,最後頁面也許就全部被這些程式碼佔據。當返回主程式時,主程式早已從頁面中消失,這時虛擬記憶體必須將主程式切換回來,如果主程式是迴圈構成的,又得繼續進行上述的處理。

發現問題的所在了嗎?問題就在於主程式(及各功能的頂層程式)本身所需的CPU時間幾乎不到1%,卻要勞師動眾將它從SpiROM複製到虛擬記憶體頁面上,然後才執行不到幾個指令,又被置換出去!所以當程式像洋蔥一樣,一層包一層,層級愈高的程式碼愈會形成任務分派形式的情況下,CPU的效能就被這些執行時間不到1%的程式碼給消耗掉了!那該怎麼辦呢?

一個方法是打掉重寫,用inline的方式執行函式,這樣會使得虛擬記憶體頁面切換有點像是循序進行,減少效能大起大落的問題,但程式碼編譯出來會非常巨大,這對大金想用小容量的SpiROM是完全不可行。

另外一個方法,則是多年前看的文章所引發的靈感。文章內容是探討PowerPC晶片的cache lock指令,對於多媒體處理的影響。由於多媒體資料大多為一次性且大量,如果沒有執行cache lock,將多媒體處理用的程式碼鎖定在快取內,很快的,CPU的快取便會充斥這些一次性的多媒體資料,而造成多媒體處理的效能低落!除此之外,Apple的麥金塔電腦,從68K系列的CPU轉換到PowerPC時,也是透過cache lock的方法來提昇68K模擬器的執行效率。之前研究NDS時,許多遊戲程式要播放即時影片前,都會透過NDS CPU的cache lock功能把處理程式鎖在快取內,以提昇播放的順暢度。這讓我想到,應該把這個虛擬記憶體系統當成快取系統來看待,才能在寶貴的RAM中放需要提昇速度的程式碼,而不是那些任務分派用的程式!

原先想在虛擬記憶體系統放個旗標,代表lock,這樣虛擬記憶體切換頁面時,就忽略這個頁面,頁面的鎖定就由想要留在頁面的程式碼主動進行lock好了。但一想到必須在各個系統放CacheLock及CacheUnlock之類的函式就有點暈了,再加上大腸包小腸形式的程式碼,還得要小心不能把所有頁面都lock(其它程式就不能執行了),其複雜度光想就腦袋打結了。

在反覆思考實作的方法卡了好久,只好試著反向思考,以結果來說,我們希望這些執行時間1%以下的程式碼,不要進入虛擬記憶體頁面執行,那NUC501有什麼硬體能達成呢?突然想到答案就是之前虛擬記憶體亟欲取代的SpiROM!雖然SpiROM速度不快,但能夠執行程式呀,跑這些任務分派的程式碼應該夠了!

趕忙修改ld的設定檔,增加一個NONCACHE的程式節區,而區段就在SpiROM所在的0x40000000位址上,緊鄰可被載入虛擬記憶體的程式碼之後(因為這些程式碼本身仍需要SpiROM當載體),這樣連虛擬記憶體系統都不用增加旗標什麼的,直接就能自由呼叫分散在虛擬記憶體與SpiROM上的副函式!

接下來的工作就比較麻煩,必須挑出哪個函式是屬於任務分派類型的程式碼,但排除performance senstive的函式,挑出來之後,便為它們標上NONCACHE巨集,這樣在編譯時,這些函式會自動被安排到NONCACHE節區中,屬於最大的任務分派函式main()也不例外!在編譯完成後,我緊張的把程式碼燒到開發版上測試。

結果效能提昇的數字令人驚訝,原先APSLib令人難堪的12K低速,效能一整個提昇26倍,得到312KB的讀取速度,字型顯示的處理時間也低於1秒了,雖然仍比原生讀取速度低,但已經足夠,只要專案完成時效能不是太低,我想應該不會再改進這個部份了。

那麼有關探討NUC501上虛擬記憶體實作的問題,應該就在這篇結束了。如果有在看這個網誌的朋友,也歡迎提出您的見解與問題,一起討論哦!

2010年6月18日 星期五

在NUC501上測定程式效能

在前次修改了ROM虛擬記憶體的演算法之後,雖然可以感覺到程式執行的速度有所提昇,但目測可能會隨著時間、身心狀態的不同,而使得結果誤差很大,所以著實需要能客觀測定程式改進幅度的機制,以免陷入程式愈改愈遭的命運!

使用ICE的人,可能早就使用profile取得程式的效率報告了,不過在開發環境是台拼裝車,加上程式執行的方式被改到亂糟糟的情況下,即使有ICE恐怕也無法正確取得所要的效能資料,另外我們也需要一個能讓程式delay正確的時間,而不受處理器速度影響的方法。

以前使用8-bit微處理器的時候,因為timer精度的關係,這類的需求通常是撰寫timer中斷,設定想要的中斷時間間隔,然後在每次中斷時將計數值加一,雖然可用,但當需要測定的時間精度很高時,恐怕沒有多少處理器可以承受這麼高速的中斷。

在NUC501上,timer是屬於AIC的管轄範圍,這部份因個人因素(哈),暫時還不想去處理,也不希望因使用中斷,讓目前還不知道效能問題出在哪的情況雪上加霜。另外,目前的開發流程主要是先在PC上進行,等階段功能完成之後,才移植到NUC501上去測試,使用中斷會使程式的移植性降低。

在PC上,可以透過SDL系統的SDL_GetTicks()函式(對應於Windows的GetTickCount())取得32-bit的系統Ticks值,時間精度為1ms,最長可以測到49.71天,所以如果程式跑得很慢,49.71天也真夠測得了(笑)。而在NUC501上,也裝備二組32-bit的timer可以提供類似的功能,但時間精度更高,如果使用「NUC501 IP Programming Guide」文件內的建議值,可以得到1us的Tick值,不過這麼一來最長測定時間只有4294.97秒(約71.58分鐘)。

不過為了與電腦的互換性,還是得忍痛降低精確度,方法是將Tick值除以1000就可以了。但因為ARM7TDMI沒有除法指令,使用除法會使得系統呼叫專門處理除法的函式庫,反而拖慢執行速度,所以希望可以簡單用shift的方式來達成。

以目前系統所使用的12MHz Crystal來看,利用TCSRx暫存器的PRESCALE,可以將Crystal除頻到接近64000Hz上,這樣既可以用shift處理又可延長量測時間,雖然有一點偏差,但還可接受,最長的量測時間就爆增到18.64小時了呢!

有了時間測定的功能之後,就可以測程式的效能了,由於RAM虛擬記憶體的效能不佳,所以先來測試SD卡的讀取效率吧!

測試的條件如下:

  1. 測試的時間為1秒。
  2. 以ROM虛擬記憶體的方式執行,在測試時間內重複呼叫DrvSDCard_Read函式,測試時每次讀取1 sector,每讀取一次將計數器加一。
  3. 時間到時將計數器的值除以2即可得到每秒讀取速度。

測試完畢後,得到DrvSDCard_Read函式至少有516K/sec的傳輸速率。雖然一次讀取多個sector可能會得到更好的結果,但為了節省記憶體,APSLib內的快取只有一個sector的長度,所以單測一個sector才能知道底層運作的效能如何,不過有這樣的速率應該不會拖慢RAM虛擬記憶體太多呀~~接下來就來測試ASPLib模擬的fread,同樣測試條件如下:

  1. 測試的時間為1秒。
  2. 以ROM虛擬記憶體的方式執行,在測試時間內重複呼叫fread函式,測試時每次讀取512 bytes,每讀取一次將計數器加一。
  3. 時間到時將計數器的值除以2即可得到每秒讀取速度。
最後得到令人難堪的12K/sec!天啊!難怪RAM虛擬記憶體的效能如此低落!看來問題有得找了。


後記:

為了得知效能增進的幅度,另外也重做了第一項測試,但改為不使用ROM虛擬記憶體,而將程式完全放在SpiROM下執行,看看虛擬記憶體這東西到底有沒有效果!值得欣慰的是,得到了38K/sec的執行結果,幸好這大半年東搞西搞的還是有代價的(泣),不然就要被我自己毆飛了。

2010年6月2日 星期三

ROM虛擬記憶體改進篇

開始新版GPS軌跡記錄器專案也快半年了,由於該專案並不是最高優先,所以時常有其他專案插隊,再加上大金一直沒有交出試作電路板,簡直快變成閒暇之餘的興趣案了,也因為這樣,才有機會在這上面嘗試新的想法,像之前實作的ROM/RAM虛擬記憶體便是很好的例子。

在專案沈寂許久之後,最近大金突然塞給我他弄好的試作電路板,要我測試看看,剛好可以測試之前寫的繪圖程式庫是否能動。在沒有試作板以前,都是在PC上以假想硬體的方式來寫繪圖程式,要驗證繪圖結果的時候,就用SDL寫的小程式來模擬LCD的顯示。一來可以讓PC與裝置的基本程式碼相同,能很快的將程式在二個平台移植來移植去,二來也可以透過PC端進行程式除錯,減低裝置端除錯的困難。

繪圖系統除了具備貼圖與遮罩處理以外,還具備多國語言顯示的能力。原先的規劃,是將龐大的字型資料放在SpiROM上,但大金希望程式可以選擇把字型資料放在SD卡或SpiROM,這樣在客戶想降低成本時,只要把字型資料改放在SD卡上,就可以用較小容量的SpiROM壓低價格,所以大金在測試板上的SpiROM只給了8Mbit,將來有機會產品化的話,甚至是4Mbit都在考慮之列。

為了達成這個要求,又不用寫太多程式碼,只好把腦筋放在虛擬記憶體上。首先先把字型資料填入RAM虛擬記憶體的交換檔中,以透過該系統存取字型資料。當RAM虛擬記憶體在收到換頁需求時會判斷來源,以決定要不要進行換頁,再加上RAM換頁之後,程式可以直接在正確的記憶體上取得字型資料,所以當字型資料可以存放在SpiROM時,對字型系統來說,即使加入了虛擬記憶體的操作指令也沒關係,幾乎不用修改就可以達成二對應了!所以將字型系統架在RAM虛擬記憶體之上可以省去很多麻煩。

就在把PC端驗證過的程式碼一股腦移植到NUC501上後,執行結果暴慢!顯示一小段文字竟然要8秒鐘以上!這樣的結果令人不能接受!暴慢的原因,除了RAM虛擬記憶體極度依賴的檔案系統需要改進以外,另一個問題是直接影響執行效率的ROM虛擬記憶體處理方式。由於檔案系統牽涉到很多程式碼的變動,所以先拿ROM虛擬記憶體開刀好了,看效能會不會好些。

原先在處理ROM虛擬記憶體時,採用腦殘的方式,以中斷時的PC值除以2048,用得到的商值來決定要對哪個頁面暫存器做修改。雖然簡單,但可用頁面數量少的時候,常發生目前使用的頁面被新要求的頁面給置換掉,結果消失的頁面之後又必須再進入例外置換回來的慘劇,嚴重點CPU就永遠在處理頁面置換,無法執行程式了!也因此原先Prefetch Abort與Data Abort應共享頁面的情況,被硬生生的拆散成程式頁面+資料頁面來解決這個死結問題。

可是,Prefetch Abort與Data Abort各自為政,還會產生一個潛在的問題,也就是當程式使用ldm指令,發生跨頁面資料讀取時,Data Abort會決定是否將二個頁面同時mapping出來,但只檢查自己轄區內的頁面暫存器是否已經有重複的位址,而沒去檢查Prefetch Abort是否有相同的頁面。所以當Data Abort產生的頁面與Prefetch Abort的頁面重複時,會浪費一個頁面放同樣的東西,這種情況根本不應該發生。

所以解決之道,還是需要一個較佳的演算法,來解決頁面置換的問題,並重新結合程式與資料這二個分離的頁面區。由於ARM7TDMI缺乏記憶體管理旗標,無法得知那個頁面最近被執行或修改過,因此,雖然Google可以找到很多虛擬記憶體演算法,能實作的也只有頁面先進先出的方式比較合邏輯,不過可以加一些改進。

程式預取例外作法如下:

  1. 為每個頁面設立一個衰老計數器。程式啟動時將所有的衰老計數器設為最老。
  2. 進入程式預取例外後,先找出計數器中最老的頁面,將其更新至新的頁面後,重設該頁面的計數器為最年輕的頁面。
  3. 將其他頁面的衰老值加一。

由於程式預取例外發生時一定是該程式碼頁面不存在才會發生,所以不用管是否與資料例外衝突,不過資料例外因為ldm/stm指令的關係,必須考慮頁面重複的問題,其作法如下:

  1. 與程式預取例外相同,先找出最老的頁面出來。
  2. 檢查發生資料例外的程式碼是否是ldm/stm,如果是,則檢查起始位址與結束位址是否在相同的頁面。
  3. 如果頁面相同,則進行步驟5。
  4. 如果不同,則檢查頁面暫存器中是否已有起始位址的頁面,如果有,則將該頁面設為最年輕,並將其他頁面的衰老值加一。如果沒有,則將之前找出來的最老頁面置換成新的頁面,將此頁面設為最年輕,並把其他頁面衰老值加一。
  5. 最後處理結束位址,與處理起始頁面相同,也是將頁面塗上SK-II變年輕,沒用的讓它變衰老,處理完後就可以結束例外處理。若之前找出的最老頁面已經被起始位址給用掉了,則需先重新找出最老頁面,才進行處理。
  6. 其他非ldm/stm的指令因為只會產生一個頁面位址,所以直接以步驟5處理。

不過Data Abort除了告訴我們資料需求的頁面位址以外,還有另一個隱藏的位址,就是堆疊裡存放著中斷前的PC值!那麼我們可以確定,PC所指的位址絕對是待會要執行的部份,不應被換掉!所以我們可以在Data Abort執行時,先幫PC所在的頁面擦上SK-II讓它變年輕,讓其他頁面衰老後,才去處理Data Abort接下來的事務,這樣可降低PC所在頁面可能是最老,而慘遭Data Abort處理換掉的機率。

將Prefetch Abort與Data Abort修改之後,除了確實解決之前產生的死結問題以外,Page Fault的次數也大幅減少,目測結果程式至少能快上二倍左右,甚至更快。但就跟任何一種演算法一樣,以上的方法,也會遇到最差的情況,不過整體而言,已經比之前的腦殘處理方式要有效率的多。

雖然速度已經快很多,但...還是得要加速檔案系統才行呢~~


2010年4月12日 星期一

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

自NUC501上實作ROM虛擬記憶體成功之後,便想趁著熱血還沒退燒之前,規劃RAM虛擬記憶體的實作。最原始的想法,是在SD卡上建立一個交換檔,接下來跟ROM虛擬記憶體的方式一樣,利用資料例外服務函式,透過交換檔完成記憶體的回寫、載入與mapping的動作,這樣就能在程式未察覺的情況下,順利的把程式所需要的記憶體生出來!

為了達成這個目的,最基本的,就是需要一個能夠處理檔案系統的程式庫。從NUC501的資料上得知,NUC501有已經編譯好的MiniNVTFAT程式庫,能夠讓我們在NUC501上處理檔案系統,經過一番考慮之後,讓我打消原先想移植自己寫的APSlib到NUC501上的念頭,雖然MiniNVTFAT沒支援長檔名,也不是什麼大問題,畢竟人家有寫好的東西幹麼不用呢?

但進行MiniNVTFAT初步測試時,便遇到莫大的阻礙,ld一直說無法連結某個section,在檢查程式庫的組成之後,才發現這個程式庫是用ARM Developer System編譯的,且因為採用不同的section命名,所以ld才無法連結!原本想自行重新編譯程式庫,CD上卻沒附上MiniNVTFAT的原始程式...好吧,看來還是維持原案,移植APSlib到NUC501上了。

不過APSlib從出生到現在,雖然是堪用的狀態,但內部已存在的bug一直都沒有修正,再加上大量使用malloc/free函式,除了拖慢速度以外,我也不確定目前所使用的GCC程式庫是否能正確執行malloc/free(畢竟這套開發系統是從其他系統硬改來的),所以想改成靜態陣列的方式,並縮小記憶體的使用量,剛好可以趁這個機會好好地把APSlib整理一番。

在這邊介紹一下APSLib,APS全名是Adaptability PocketOS Serve(適應性小型作業系統服務),主要的目的,是當時為了能應付老闆經常變化的硬體軟體規格,希望能寫一套不管硬體怎麼變化,軟體需求怎麼變化,都有適應性的檔案處理程式庫,所以規劃出幾個模組:

  1. 最底層的I/O驅動程式,提供基本的讀寫功能。這樣可以在硬體尚未完成前,先在PC上利用fake file模擬磁碟機或記憶卡的運作,等硬體完成之後再改成控制該I/O實際運作的驅動程式。在NUC501的移植上,這部份可以直接呼叫DrvSDCard程式庫來處理,只要幫DrvSDCard弄個殼層,使其相容APSlib就好了,這是APSlib移植工作最輕鬆的部份。
  2. I/O管理服務,其運作方式就如同PC上的BIOS INT 13h,提供上層直接存取磁碟的服務。由於之前記憶卡插拔的相關處理沒做好,所以趁這個機會加上去,快做完了才發現開發版上SD Detect沒接,DrvSDCard程式庫也沒偵測...
  3. 統一快取,架構在I/O管理服務之上,目的是為解決上層程式需要巨大緩衝區,卻沒有足夠記憶體的困擾。
  4. 檔案系統過濾器(file system filter),主要提供檔案系統的管理,檔案過濾器包含一個呼叫通道供上層使用,舉凡開檔、關檔、目錄處理等相關的檔案動作,都是過濾器提供的功能。處理過程中過濾器需將原生檔案系統的資訊,轉換為APSlib自訂資訊,這樣不管哪個檔案系統都會傳遞一致的資訊,異種檔案架構便能以相同的介面處理,而上層程式就不需要額外關心某些檔案系統的細節了。
  5. 檔案系統過濾器服務,負責管理檔案系統過濾器,並將過濾器提供的功能,以API的形式供上層呼叫。
  6. 系統服務,負責進行目錄分析、模擬已知的函式庫功能,如fopen等。

現在想想這樣的架構不太適合小系統,應該要再簡化才是。移植過程中,也順便思考RAM虛擬記憶體的實作細節,不過愈是深入思考,愈覺得透過資料例外的方式達成很困難。主要的原因是:

  1. APSlib及相關程式碼必須存放在RAM中,才能避免指令預取與資料例外重入問題,而RAM扣除虛擬記憶體使用的頁面已經剩下不多了,根本放不下!解決之道是把APSlib放在SpiROM中執行,但發生swap時效能可能會慢到不行。
  2. 就算把APSlib放到SpiROM中的執行效能可以接受,APSlib的某些設計會借用堆疊暫時存放資料,而例外能提供的堆疊很小。就算擴大,也會因為這區域必須保留而嚴重壓縮到可用的變數空間。
  3. ARM7TDMI沒有記憶體讀寫的管理旗標,無法知道頁面是否已修改過,所以為了防止資訊流失,只好假設頁面被修改過,那頁面切換時,就一定要執行回寫的動作。當發生大量只讀取虛擬記憶體時,對效能的衝擊會很大。
因為以上幾點的問題還蠻棘手的,所以用例外主動切換頁面的方式可以說無法運作,那怎麼辦呢?想想只好改為被動執行,由程式主動告知何時該切換頁面。因為不想弄得太複雜,所以以最簡單的方式,只要三個函式來處理虛擬記憶體就夠了:

bool InititalVM(void); // 初始化虛擬記憶體

// 要求以唯讀方式鎖定虛擬記憶體
_ULONG vmLockReadMemory(void *pVMAdr,_ULONG ByteCount);

// 要求以寫入方式鎖定虛擬記憶體
_ULONG vmLockWriteMemory(void *pVMAdr,_ULONG ByteCount);

其運作方式如下:
  1. 在link script中,建立一個.vm的section,然後把需要放在虛擬記憶體的變數,都安排在這個section內。目前我們指定的位址是在0x04000000的空間上。
  2. 初始化虛擬記憶體時,在記憶卡上建立一個交換檔,並將.vm的資料從SpiROM全部寫到該交換檔內,這樣就等於完成.vm區的變數初始化。
  3. 當程式需要使用虛擬記憶體內的變數時,程式必須告訴vm管理程式,所需的記憶體位址與長度,屬性是要寫還是唯讀,程式可以透過vmLockWriteMemory及vmLockReadMemory來要求。mapping成功後,這二個函式會回報實際可用的長度,讓程式可以在安全範圍內讀寫虛擬記憶體。
  4. 當執行vmLockWriteMemory及vmLockReadMemory時,函式內會先調整程式需求的長度,由於目前可用的頁面只有二頁,所以如果超過二個頁面能呈現的長度(目前是4096 bytes)就要進行調整,再來檢查頁面資訊與新頁面是否相同,如果不同,vm管理程式會依頁面的屬性來決定是否透過fseek及fwrite的方式將該頁面資料回寫到交換檔中,最後將RAM mapping到新的位址上,再透過fseek及fread將新頁面的資料載入,結束時回傳可用的長度給呼叫的程式。
  5. 程式得到可用的長度後,便可以愉快的在安全範圍內直接使用虛擬記憶體變數了。

以下是簡單的程式範例:
// 存放在虛擬記憶體的變數
__attribute__((section(".vm")) _ULONG TestVM[256];


// 鎖定記憶體,且屬性是寫入,長度為4 bytes
_ULONG cLock=vmLockWriteMemory(&TestVM[7],sizeof(_ULONG));

// 直接存取該變數
TestVM[7]=0x12345678;

只要遵守這樣的規則,用起來其實還算簡單,也不會增加太多程式碼,效能上就必須由程式主動關心了,比方說在虛擬記憶體的排列上多費點心思,可以大幅減少交換檔的讀寫動作。

既然已經有了簡易的虛擬記憶體系統了,也可以用堆疊的方式做簡單的動態配置,只要把交換檔設大一點就好了。

// 從虛擬記憶體要求一塊長度為ByteCount的記憶體
void *vmPushBuffer(_ULONG ByteCount);
// 將配置的記憶體數量還回去
void vmPopBuffer(_ULONG ByteCount);
  1. vmPushBuffer只要將堆疊指標減去ByteCount,並回傳該位址在虛擬記憶體內的位址即可。
  2. vmPopBuffer只是簡單的把ByteCount加回堆疊指標。
由於這個功能沒有進行檢查,所以一切都需要由程式自行把關,以下是使用範例:

_UBYTE *pVMBuf=vmPushBuffer(262144);//配置256K的記憶體
_ULONG ByteLen,cLock;

ByteLen=262144;

// 清除配置區內的資料

while(ByteLen)
{
cLock=vmLockWriteAddress(pVMBuf,ByteLen);
memset(pVMBuf,0,cLock);
pVMBuf+=cLock;
ByteLen-=cLock;
}

// 用完配置區就歸還

vmPopBuffer(262144);

雖然整體的解決方案看起來有點可笑,不過完整的管理程式也代表會多耗費記憶體,這對我們希望記憶體盡量留給其他程式的需求不合,所以能用簡單的方法擴充可用記憶體,而且不難用才是我們要的結果囉~~

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造成的效能衝擊就可大幅降低,程式可以執行的更快、也更省電!不過目前也只是想想,還需要做些實驗才知道這個方法是否有用了。

2009年3月6日 星期五

單核心多執行緒會有效益嗎?

為了尋找某主機傳說中的關鍵,我受到了勇者的請託,必須寫程式嘗試解除重重的枷鎖,才能打開光明之門,取得寶箱!不過用暴力攻擊法攻擊那些鎖大約需要10年的時間,我才會知道有沒有答案(暈)!而且也只是冰山一角。最終還是必須以現有資料推演的方式去找出關鍵,希望能借助電腦強大的運算能力,順利的解決。

目前資料推演的方式,是必須跑2的48次方個迴圈,來嘗試各種不同的組合,這是個很笨的方法,但我天真的認為,現代CPU的速度要跑這樣的運算,簡直是一塊蛋糕!在很快的寫了個單執行緒的程式去跑之後,我發現我錯了。用Intel Core 2 Duo 1.86G去算,竟然8小時也跑不完!後來才發現,光跑2的32次方就大約10分鐘,這樣的運算要跑65536次,所需的時間是655360分鐘,也就是10922.67小時,換算成天數的話,只要455天就可以跑完了!

如果455天後才知道答案,我想我會先被砍吧?!正苦思該怎麼加速運算時,想到可以善用多核心的機制。由於各種組合之間沒有相關,很適合將運算的迴圈分割,以並行的方式來執行。所以我把單執行緒的程式,改成一次發出4個運算執行緒,然後主程式作為執行緒監測用,每10秒印出各執行緒目前的運算進度,果然速度大幅提昇,10分鐘至少可以完成4組運算,真是感謝多核心的進化呀!不過系統反應就變非常遲鈍了,唉~看來是必須開電腦牧場還是去借超級電腦(笑)來找答案了。

剛好身邊有台超過10年,暱稱Serina的精英DeskNote電腦,CPU是VIA Samuel 733MHz,跑Windows 2000。雖然很慢(不是現採哦!)風扇聲又大,不過還是可以幫忙算些零星的部份,減少整體時間。由於CPU是單核心,我想多執行緒反而更慢完成吧?

因為懶惰改程式,所以便4執行緒直上。從印出的執行緒進度來看,單核心電腦在(Windows 2000)分配每個執行緒的執行時間都還蠻平均的,計算數值的差距不大,而雙核心(Windows XP)則是有些執行緒的進度會超前,不過也許這是作業系統使用不同排程的關係。

最後Serina花了99分鐘完成8組運算,由每個執行緒分別計算2組運算工作。聽著Serina的風扇怒吼,為了不太難為她,所以我又為Serina改了一組程式,同樣也是完成8組運算,不過是2執行緒的,每個執行緒分別計算4組運算工作,最終2執行緒版本用了114分鐘完成,比4執行緒還慢!

沒想到即使是單核心,如果運算分割得當的話,也可以引出更高的運算效能出來呢!等會兒在C2D上試試看8執行緒會不會更快,呵呵。

不過這樣下去要算到何年何月呢?有沒有更快的方法呀?

〔後記〕剛測4執行緒,又落在113分鐘完成!看來是樣本數不夠多囉~~

2008年6月3日 星期二

Subversion

好不容易公司的專案告一段落, 又開始我的網路fool around, 不過這次還真的是找到有用的東西...

由於程式設計生涯常常會遇到程式版本的問題, 比如說改東改西, 改到爛掉, 某些版本有新功能, 有些則沒有...一個人寫的時候還好, 我都是在做愚蠢的事之前, 先用WinRAR備份整個專案目錄, 然後才進行修改...之前備份用的檔名都是檔名加date code, 可是在尋找舊版本時, 看到一堆包含date code的檔名, 瞬時就頭昏眼花...後來只好捨棄date code, 改為在做新的變更時, 先把舊的備份檔名後面多加上'-', 然後才備份, 這樣一來, 最新版本的前一次備份就是後面沒加上'-'的備份檔案, 而愈舊的檔案後面的'-'愈多, 如果需要日期, 只要看檔案建立時間就好, 多虧了檔案總管的排序功能, 可以很快的找到某個版本的備份, 看起來就像這樣...



不過這樣也有問題, 當後面的'-'太多的時候, 光是改檔名就改到手軟..所以每隔一段時間就得砍掉太早版本的備份, 這麼多年下來, 這方法也沿用至今, 不過在專案是二人以上合作的時候, 這樣的版本控制就相當的麻煩...

為了解決這個問題, 之前很幸運的找到一款軟體 WinMerge, 這是一款可以比較二個目錄下的檔案, 並列出不同的地方, 然後你可以直接用WinMerge進行二個檔案的合併...不過麻煩的就是, 總是要"人"來做、來分析程式碼中哪些是要合併的, 哪些是冗碼, 如果忘了自己修改過的部份, 又直接拿同事的版本來用, 最後發現自己寫的部份不見了, 又得從備份還原, 不僅勞民傷財, 最慘的是, 等到不知道第幾個'---'之後才發現, 那才真是愈哭無淚~~

雖然目前我一個人做專案, 但哪天與別人合作時, 便必須有個機制讓雙方可以隨時向對方提供更新程式碼, 而不會再丟三落四...這時我又想到了開放原始碼界常用的CVS, 之前我對它是非常排斥的, 因為很麻煩的要記指令, 不過在沒多餘的錢買其他版本控制軟體的情況下, 還是搜尋了一下CVS, 不過CVS好像沒有Windows版本的, 而Wiki告訴我, 號稱可以從CVS無痛轉換的SVN有, 那就趕緊來試用看看囉!!

祭出了VMware, 在虛擬的XP下安裝了SVN之後, 便隨便拿出一個專案來惡搞(當然是有備份囉)...雖然還是必須下指令, 不過說明訊息竟然是中文的呢!!先利用svnadmin建立資源庫, 並將專案用svn import之後, 再進行checkout便可以開始使用了(有點小麻煩~~), 不過我只有先用本機的資源庫進行版本控制的測試, 還沒用到最終的應用WebDAV, 有空再來試試~~

之後進行模擬二地同時進行修改, 某方先更新版本之後, 另一方要更新版本時就會出現禁止更新, 除非先做update...svn在update時便會將對方寫的部份合併到自己的工作副本中, 同時也保存了自己改寫的部份, 如果二人都對同一行程式碼進行修改, 就會產生衝突, 這時還是要人的介入與協調來決定最正確的方式, 解決衝突之後便可利用svn commit送出變更, 這時版本就會一次一次的加上去...如果想知道每次版本的變更情況, 也可以利用指令取得版本間的差異, 真是相當好用!!沒想到我真是白混了, 這麼好用的東西沒早點導入~~

如果不喜歡下指令, 還有好用的svn gui軟體, TortoiseSVN, 是svn的client端軟體, 不用說, 二者產生的資源庫是可以共用的, 同時也有中文化的patch, 有興趣的可以看看哦~~

2008年3月6日 星期四

綠乖乖!?

哎呀, 許久沒有更新網誌了說~~(又來了~~)

最近都在忙工作上的事情囉, 所以光想網誌要打什麼就傷腦筋, 只好期許自己今年一定要多多的增加網誌的數量囉...不過文章在精不在多, 看官們就多多包涵啦~~

這幾天一直處理巨盛CSC3800的問題, 這是顆屬於多功能的OTG單晶片系統, 可以操作二組USB磁碟, 加上IDE, CF, SD, MS等介面, 還有MP3及WMA的decoder, 功能可以說是包山包海, 我的工作就是利用這顆寫一套程式, 可以從CD/DVD複製檔案到USB/SD/MS, 其實就類似外面在賣的OTG硬碟盒之類的, 只不過我們的產品裡面裝的是一台DVD, 而不是硬碟就是了...

其實這工作從去年就開始了, 算算到現在也滿一年了, 從拿到開發板之後發現程式庫沒有完整的原始程式可供參考, 到實際開發時才發現廠商沒給完整的datasheet(說什麼希望我們直接用程式庫來處理, 問題是程式庫沒有處理光碟的程式啊~~), 好不容易東拼西湊的把程式架構弄出來, 卻是這也不支援那也不行, 要求了幾次的程式庫更新才收到一次更新, 好不容易自力更生的把程式弄了出來, 一連數次的洗好板子, 焊上晶片就是不會動, 這專案能拖這麼久我也覺得不可思議..

所以這個專案再動, 就非把它弄好不可..透過降低CPU的頻率, 終於得到了堪稱穩定的執行能力, 雖然效能折損一大半, 不過穩定的平台才好開發程式嘛~~這幾天就開始抓蟲, UI也被我東補西湊的把缺乏的部份補上, 就在順利的處理完SD/MS卡的傳輸之後, 我又碰上了暗礁~~卡在USB傳輸上~~

基本上巨盛的想法我是同意的, 不讓客戶自行針對暫存器做低階處理, 有助於確保客戶的專案能正常動作, 簡化開發流程, 同時在轉移專案到新晶片上時, 還可以維持source compatible的能力, 問題就在於他們能否確保程式庫完全正確無誤??亦或者能在客戶自己設計的電路上正常執行??

結果是, 沒晶片完整資料, 不知道哪邊程式設定有錯誤, 沒程式庫原始程式, 不能debug也不知道錯在誰的身上, 每次當在程式庫裏都苦惱萬分..這次也是~~算了, 反正有工程需求單可以填, 所以今天就弄了個會出問題的小程式丟給他們去測囉, 我還給他們完整的原始程式喔, 不過目前還沒下文就是了...

在等待回覆的同時, 無聊逛逛BNW網站, 看到了這篇乖乖的妙用, 看來我是不是也要去買包綠乖乖, 來讓我的專案能順利進行呢~~

於是乎~~



哎呀~~老天保祐啊, 綠乖乖保祐啊~~讓我的專案順順利利的吧~~~

不過我等下就要吃掉它了~~

2008年1月21日 星期一

CC65與VT1682的探討

雖然部落格名為"遊戲與工作與生活的...", 可是卻從來沒寫過工作相關的事, 看來今年是有必要改變一下啦~~所以今年的第一篇部落格就以工作開始吧, 希望能有好成績出現囉~~

由於大金最近想開發可以在各大賣場及夜市獨立販售的電視遊戲, 便向相關廠商訂了一片VT1682開發板, 所以最近都在開始研究這塊開發板...基本上這個開發板所採用的CPU架構, 正是我之前一直想學的6502(所以ID也加上6502), 不過一直苦無機會, 現在終於可以一探6502的魅力究竟在哪裡...

不過在看了6502的databook及研究了相關資料, 我只能說6502真的是很8-bit的CPU, 除了stack只有256 bytes(因堆疊指標只有8-bit!!), 連能用的暫存器都只有A, X, Y三個, 而且大部分的指令都是隱含A, 也就是只能對A動作, 而且由於暫存器短少, 因此6502也有很豐富的記憶體存取指令, 一堆在計算機概論上可以讀得到的定址模式, 6502統統都有, 想怎樣都行..可是這樣一來, 6502對記憶體速度的要求相對的就高出許多, 在執行16-bit及32-bit的運算時, 更是對效能的打擊(代表更多的記憶體存取), 可是好歹當年也只是打不過Z-80罷了, 可見這顆CPU設計的獨到之處..這在指令表中似乎可以看出一些端倪, 除了指令精簡以外, 就好像透過這些指令就能組合出超神奇的演算法一般, 不然當年也不會有那麼多的能人異士在6502上開發出許多有趣的東西了...(謎之聲:說不定人家只是不得已的說~~)

來說說VT1682的規格吧, 這是顆單晶片封裝的電視遊戲解決方案, 裡面包含二顆6502(哇!!8-bit CPU雙核心耶!!), 跑不同速, 主CPU跑5.5MHz, 跑得不是很快, 被用來處理遊戲程式與動畫等等, 不過副CPU就可以跑到21MHz..我有問過為什麼副CPU需要跑這麼快??具相關人士的回答是說, 因為廠商希望用副CPU跑wavetable的處理, 以及與週邊通訊相關的處理...聽起來好像不錯, 不知道實際的處理情況如何~~


由於8-bit的處理器只能定址64K的空間, 因此VT1682擁有超複雜的bank切換設定, 最大可以支援到32MB的ROM空間...此外內建8K的RAM, 還需與副CPU共享其中的4K(好小的RAM), 圖形處理器的解析度只有256x240, 不過有雙重捲軸的顯示能力, 數種發色設定(32768色(一面), 256色(捲軸二面),16色(捲軸二面)), 還有最大240個動畫拼合的顯示實力(8x8, 8x16, 16x8, 16x16, 水平線上16個), 若VT1682早20年出現的話, 肯定是當時最強的8-bit主機平台...只可惜現在只被用來處理小朋友的電腦輔助教學等等的應用...

在VT1682的設計上, 我認為設計VT1682的人受到任天堂紅白機的影響頗深(該公司也有好幾顆相容晶片產品), 在硬體架構可以說是紅白機架構的延伸, 包括遊戲圖形是放在ROM上, 而不是現代遊戲機設計上所具備的VRAM這點, 多多少少限制了可用的程式方法來創造特殊的圖形展現...而且暫存器排列雜亂無章也是我所抱怨的, 說明文件一團亂, 廠商還沒提供基本的程式庫咧!!天啊~~看來我得自求多福了說~~得努力的建立底層程式庫才行...


雖說8-bit的CPU本來就應該用組合語言撰寫程式, 這樣才能確實發揮效能, 不過為了讓程式具備可移植性, 還是得用C才行~~還好在網路早就有許多人愛用的CC65, 可以產生數種6502機器(如appleII)的執行程式, 這當然也包括紅白機, 可是VT1682根本就不與紅白機相容(只是架構像而已), 這就表示我得自己編譯CC65的程式庫, 還得為VT1682寫crt0才行...

這幾天就都在搞這些事情, 建立VT1682的暫存器定義, 修改紅白機的crt0, 讓它可以使用在VT1682上, 搞懂CC65的編譯方式(這樣才能用DevC++的IDE介面進行自動化編譯), 編譯標準程式庫供CC65的執行基本需求, 以及研究VT1682的bank切換方式等等, 真是一個頭二個大...好不容易有些心得了, 得紀錄一下:

1.VT1682在存取位址0xE000~0xFFFF的時候, 會強制位址線到0x1FE000(TP13-TP20=1), 不過由於program bank0 selector的初始值為0, 因此當執行此區的程式時, 該位址實際上會被bank switcher用BANK0_REG3的bit7-bit6取代TP20-TP19, 變成0x07E000, 所以crt0及中斷向量必須安排在0x07E000~0x07FFFF的區域, CPU才能正確的開始執行(說明文件不清不楚的, 我還以為說明文件寫錯了說), 由於這區域會可以讓它固定出現在0xE000的空間,因此此區也可以存放固定的服務函式(如GBC時代的ROMBankCall), 不過這樣ROM最大只能有4Mbit, 若想達成支援到32MB的ROM空間, 則必須考慮遊戲實際的ROM大小, 與program bank0 selector切換的情況, 如果設定讓TP的位址線依據換頁暫存器的值輸出時(mode 7), 而不與BANK0_REG3混合的話(PQ37-PQ30, mode 0), 就必須將0x07E000的資料複製一份到0x1FE000, 以防止切換到mode 7時, 其實際位址線會立即由0x07E000切換到0x1FE000, 而引發程式不能繼續執行的問題..也就是說, 如果需要用滿32MB的ROM, crt0區段就必須有17個複製品(1個啟動用, 剩餘16個(因為PA24-PA21)用來防止換頁時導致此固定碼空間消失的問題), 這樣就能確保需要執行固定碼功能時, 這些程式碼都會固定在0xE000上出現...

2.由於CC65在link phase時, 需要提供memory configuration file, 而這個設定檔之前都看不懂, 好不容易上禮拜才理解它的意思:

1)在MEMORY area中所描述的是如果有程式需要放在該區段時, 該區段的起始位址(START)及該區段可容納的長度(SIZE), 如:

memory{
ROMBANK0: start = $C000, size = $2000;

}
這樣就表示有個記憶體區段ROMBANK0, 放在這區段的程式碼, 其基底位址都是從$C000開始的(這樣換頁執行時, 該程式碼只能出現在$C000區段, 否則不能正常執行), 另外還有其他的輔助設定值...


memory{
ROMBANK0: start = $C000, size = $2000, file = %O, fill = yes;

}

file可以用來單獨輸出該區段的binary到指定的檔案上, 如果我寫file = "rombank0.bin", link時就會輸出一個rombank0.bin的檔案, 方便你去燒ROM或分析還是怎樣, 如果用%O, 則代表不輸出該區段, 而只將該區段資料輸出到最終的binary file中..而fill則表示需要將該區段的未用區域填滿, 這樣就能確保產生出來的binary code會出現在正確的位址上...

2)memory area每填一組資料, 在link時便會產生出該區段, 如果我們要產生4個32K的區段(共128K), 則要寫4組

memory {
ROMBANK0: start = $8000, size = $8000, file = %O, fill = yes;
ROMBANK1: start = $8000, size = $8000, file = %O, fill = yes;
ROMBANK2: start = $8000, size = $8000, file = %O, fill = yes;
ROMBANK3: start = $8000, size = $8000, file = %O, fill = yes;
}

linker會依寫的memory區段順序, 產生binary file, 而在該區的程式碼都會以$8000為基底位址

3)segment area則在指定程式及資料所屬的記憶體區段, 基本上CC65有幾個固定的segment必須提供:

memory {
ZP: start = $00, size = $100, type = rw, define = yes;

: 放滿需要將ROM0推擠到0x7E000區段的其他頁面

ROM0: start = $E000, size = $1FF4, file = %O, fill = yes, define = yes;
ROMV: start = $FFF4, size = $C, file = %O, fill = yes, define = yes;

: 放滿在ROMV之後的其他頁面

SRAM: start = $0100, size = $0300, define = yes; (CC65模擬大堆疊的空間, start表示堆疊的bottom位址, size表示堆疊的空間大小)

RAM: start = $0400, size = $0C00, define = yes; (CC65處理變數區域的空間)
}


segments {
STARTUP: load = ROM0, type = ro, define = yes; (crt0所在的區段, 也就是程式的進入點)

INIT: load = ROM0, type = ro, define = yes; (與crt0相同)

VECTORS: load = ROMV, type = rw, define = yes; (中斷向量表的位址)
ZEROPAGE: load = ZP, type = zp; (zeropage的segment, 基本上CC65把256 bytes的堆疊空間當儲存返回位址及暫存器變數來用)

CODE: load = ROM0, type = ro, define = yes; (其他可以放在固定區的程式碼都屬於這個節區)

RODATA: load = ROM0, type = ro, define = yes; (其他可以放在固定區的資料都屬於這個節區)

DATA: load = RAM, type = rw, define = yes; (有初始值的RAM data會放在這個節區)

BSS: load = RAM, type = bss, define = yes; (無初始值的變數會放在這個節區)
}

4)要指定程式的區域, 必須在資料區域開頭寫上

#pragma rodataseg ("segments內指定的節區名稱")

然後在程式區域的開頭寫上

#pragma codeseg ("
segments內指定的節區名稱")

這樣程式碼與資料都會被連結到指定的節區內, 不過要使用這些名稱, 在設定檔中必須加上define = yes, 才可以被取用..

5)由於6502只能有256 bytes的堆疊, 因此CC65很厲害的自己用程式來模擬大堆疊, 這樣才能對應大多數C編譯器所採用的方法, "透過堆疊傳遞函式參數",
而原先的堆疊空間則拿來當作快速存取區(當暫存器用), 以及執行JSR指令時存放返回位址(基本功能), 不過處理大堆疊的相關程式碼會使用固定位址的zeropage空間, 來輔助處理(畢竟暫存器數量不多), 同時這些處理函式也沒有防止中斷的機制, 這就表示, 這些函式不能被重入, 如果要透過中斷在背景處理一些東西, 則必須確保這些zeropage的空間被正確的備份, 因為CC65所編出來的程式碼都會用到這些固定函式及固定空間, 沒能正確的處理肯定當得很高興...


這就表示, 所有中斷處理副程式都必須用組合語言小心的處理zeropage的變數之後, 才能呼叫由C語言寫成的中斷處理程式...而且還必須改良堆疊處理函式讓它們在處理stack push及pop時, 不被中斷~~

雖然可能還有其他的問題, 目前是還沒遇到, 不過我對於stack也可以用程式模擬感到驚訝, 或許這就是6502生命週期如此長的關係吧, 因為什麼都有可能呢..