精華區beta Emulator 關於我們 聯絡資訊
http://fms.komkon.org/EMUL8/HOWTO.html 原文是Marat Fayzullin這個作者寫的, 看大陸的討論發現他好像是一個很強的模擬器開發者..@@ 下面是大陸網友的重點整理, 想自己開發模擬器的人可以看一下, 我是看不懂啦.. *********************************************** 綱要: ‧什麼可以被模擬? ‧什麼是 emulation,它跟 simulation 有什麼不同? ‧模擬有專利的硬體,是合法的嗎? ‧什麼是直譯式的模擬器,跟編譯式的模擬器有何不同? ‧我想寫一個模擬器,我該從何開始? ‧我該用哪一種程式語言? ‧我從哪裡可以得到想模擬的硬體的資訊? 實做: ‧如何模擬一個 CPU? ‧如何存取被模擬的記憶體? ‧週期性的運作有哪些? 程式技巧: ‧如何最佳化 C 程式碼? ‧什麼是高低字節順序? ‧如何讓程式具可移植性? ‧為何我要模組化我的程式? ------------------------------------------------------------------------- 什麼可以被模擬? 基本上,任何東西有微處理器在裡面,就可模擬。當然,只有那些可以跑程式裝置,我們 才有興趣模擬。包括: ‧電腦 ‧計算機 ‧遊樂器 ‧大型電動 ‧其他...... 必須特別註明,你可以模擬任何電腦系統,即是事非常複雜的系統(譬如 Amiga 電腦) ,但是執行效率可能很低。 ------------------------------------------------------------------------- 什麼是 Emulation,它跟 Simulation 有什麼不同? Emulation 模擬裝置內部的硬體,Simulation 是模擬裝置內部的功能。舉例來說,一個 程式模擬小精靈大型電動的硬體,然後執行小精靈的 ROM,就是個 emulator。一個小精 靈的 PC 遊戲,就是個 simulator。 ------------------------------------------------------------------------- 模擬有專利的硬體,是合法的嗎? 這是個灰色地帶,只要你不是透過不合法的管道,拿到硬體的資訊,就應該不違法。但是 很清楚知道,跟模擬器一起散佈有著作權的系統 ROM(例如 BIOS),是違法的。 ------------------------------------------------------------------------- 什麼是直譯式的模擬器,跟編譯式的模擬器有何不同? 模擬器有三種設計的方式,這些設計也可以混用,來達到最好的效果。 ‧直譯式 模擬器一個位元又一個位元的,從記憶體讀取代碼,然後解碼,執行對應的暫存器、記 憶體、輸出入的命令。通用的演算法如下: while (CPUIsRunning) { Fetch OpCode Interpret OpCode } 這種設計的好處是,容易除錯,容易移植,容易同步(你只需要計算過了多少 CPU 週期 ,然後讓你模擬的其他部份,跟 CPU 同步)。 這種設計明顯的弱點,就是執行效率很差。執行直譯會花很多 CPU 時間,你會需要很快 的電腦,才能有不錯的執行速度。 ‧靜態編譯式 這種技術,就是把一支你要模擬的系統的代碼,編譯成你的電腦的的組合語言。編譯的 結果,通常是一支你的電腦的普通執行檔,不需要額外的工具就可以執行。靜態編譯,聽 起來很美好,但通常不可行。例如,你就無法靜態編譯會自我修改的代碼,因為這種代碼 只有執行時,才會知道內容是什麼。為瞭解決上述的問題,或許需要混用直譯器,或是動 態編譯編譯器。 ‧動態編譯式 動態編譯基本上跟靜態編譯一樣,但動態編譯發生在程式執行時。動態編譯是在執行到 CALL 或 JUMP 時才編譯,取代一開始就編譯一整個程式。為了增加執行效率,這種技術 常常結合靜態編譯。你可以讀,動態編譯式麥金塔模擬器的作者 Ardi,的這篇動態編譯 白皮書學到更多 ------------------------------------------------------------------------- 我想寫一個模擬器,我該從何開始? 想要寫一個模擬器,你必須懂程式設計,以及數位電子。如果懂得組合語言,會更好。 1.選一種程式語言 2.找到所有被模擬硬體的所有資訊 3.寫 CPU 模擬,或是選用一個現成的 CPU 模擬程式 4.寫個粗略的其他周邊硬體的模擬,至少要一部分 5.在這個時候,寫個內建除錯器,讓你可以暫停模擬,檢查程式執行的結果。你也會需要 一個被模擬 CPU 的組合語言反組譯器。如果找不到現成的,就自己寫一個。 6.試著用你的模擬器執行程式 7.用除錯程式跟反組譯器,看看程式到底在幹麼,然後根據此修改你的模擬器 ------------------------------------------------------------------------- 我該用哪一種程式語言? 最常被用到是 C 跟組合語言,各有優缺點。 ‧組合 語言 + 通用,可以產生速度快的程式碼 + 可以直接使用暫存器,來映射被模擬的暫存器 + 很多組合語言指令,可以對應到被模擬的組合語言指令 - 程式是不可移植的,換句話說,你的模擬器,不能在別種 CPU 上跑 - 很難除錯跟維護 ‧C 語言 + 可移植性,所以可以在不同的作業系統上跑 + 相對容易除錯跟維護 + 對硬體的不同假設,可以很快的測試 - 通常 C 語言的程式比組合語言的程式慢 要寫模擬器,對所選擇的語言,瞭解得很透徹,是絕對必要的。因為模擬器的程式很複雜 ,你要最佳化你的模擬器,讓它跑得越快越好。電腦模擬器程式,絕對不是你越來學習程 式語言的專案。 ------------------------------------------------------------------------- 我從哪裡可以得到想模擬的硬體的資訊? 下列地方,你會想去看一看: 網路新聞群組 ‧comp.emulators.misc 這個新聞群組,討論模擬器一般的問題。許多模擬器作者會訂閱,雖然裡面雜音很多。 如果要貼問題到這個新聞群組,記得先看 c.e.m FAQ 常見問題。 ‧comp.emulators.game-consoles 跟 comp.emulators.misc 一樣,不過這個新聞群組,專攻電視遊樂器的模擬器。如果要 貼問題到這個新聞群組,記得先看 c.e.m FAQ 常見問題。 ‧comp.sys./emulated-system/ comp.sys.* 新聞群組階層,專攻特定的電腦系統。你閱讀這些新聞群組,可以得到有用 的技術資料。典型的例子: com.sys.msx MSX / MSX2 / MSX2+ / TurboR 電腦 comp.sys.sinclair Sinclair ZX80/ZX81/ZXSpectrum/QL comp.sys.apple2 Apple ][ 如果要發問題到這個新聞群組,記得先看 FAQ ‧alt.folklore.computers ‧rec.games.video.classic FTP ‧ Console and Game Programming ‧ Arcade Videogame Hardware ‧ Computer History and Emulation WWW ‧Marat Fayzullin Homepage ‧Arcade Emulation Programming Repository ‧Emulation Programmer's Resource ------------------------------------------------------------------------- 如何模擬一個 CPU? 首先,如果你需要模擬一個標準的 Z80 或 6502 CPU,你可以使用 Marat Fayzullin 所 寫的 CPU 模擬器 當然有些限制。 對那些想要自己寫 CPU 模擬核心,或是對其中的運作原理感性趣的人,我提供一個用 C 寫的範例架構如下,在真正的實做,你或許會考慮略過其中部份,或添加新的部份。 Counter=InterruptPeriod; PC=InitialPC; for(;;) { OpCode=Memory[PC++]; Counter-=Cycles[OpCode]; switch(OpCode) { case OpCode1: case OpCode2: ... } if(Counter<=0) { /* Check for interrupts and do other */ /* cyclic tasks here */ ... Counter+=InterruptPeriod; if(ExitRequired) break; } } 首先我們指定 CPU 週期記數器 (Counter),以及指令位址記數器 (PC) Counter=InterruptPeriod; PC=InitialPC; Counter 紀錄了到下一次系統中斷髮生,還剩多少個 CPU 週期。注意當 Counter 過其實 ,系統中斷不必然發生。你可以利用他來處理其事情:像是時鐘同步,更新螢幕的掃瞄線 等。等等,我們會討論這些。PC 則紀錄了CPU 會從那個記憶體位址,讀取下次的執行的 指令。 在我們給這些設定初始值之後,然後開始進入主迴圈: for(;;) { 主迴圈也可以寫成這樣: while(CPUIsRunning) { CPUIsRunning 是個布林值,這樣寫有個好處,你可以在任何時候,設 CPUIsRunning=0 ,來終止主迴圈。然而在每個迴圈檢查這個變數,會花不少的 CPU,而我們應該儘量減少 花費 CPU。同時,不要寫成下面這樣子: while(1) { 因為這樣寫,編譯器產生代碼,去檢查 1 為 "真" 或 "假",你不會希望在主迴圈的每個 迴圈,都去執行這多餘的動作。 現在我們在主迴圈內,第一件事,就是去讀下一個執行碼,然後修改程式位址記數器。 OpCode=Memory[PC++]; 注意,這是最簡易的方式,來模擬讀取記憶體,但並非永遠可行。更通用的方式,來存取 記憶體,稍後會提到。 在提取操作碼後,會從 CPU 週期計數器,扣掉這個指令所需的週期數。 Counter-=Cycles[OpCode]; Cycles[] 表內放的是每個操作碼,所需要的週期數。要特別注意,有些指令(例如條件 式跳躍,或是呼叫副程式),需要的週期數,是跟操作後面緊接的參數而變動。這個可以 在執行指令碼時調整。 現在該是解譯操作碼,然後跟著執行的時候了: switch(OpCode) { 有一個錯誤的觀念,認為 switch 敘述事沒有效率的,因為會被編譯成 if () ...... else if () ........ 敘述。這只有在 case 數量很少的 switch 敘述,才會被這樣編譯 。當有 100 到 200 個 case 的時候,switch 敘述通常會被翻譯成 jump 表格,jump 表 格,其實蠻有效率的。 有其他兩種替代方案,可以用來解譯操作碼。第一種方法,是建一個函式表,然後呼叫對 應的函式。這種方式,比用 switch() 沒效率,因為呼叫函式,有額外的開銷。第二種方 式,是建一個位址的表格,然後使用 goto 敘述。這種方式,稍比用 switch() 有效率一 點,但這種方式,只適合用在編譯器支援未預定位址表格。其他的編譯器,不會允許你這 樣定義表格。 在成功解譯並執行一個操作碼後,這時候該去檢查有沒有任何系統中斷髮生。這時候,你 也可以執行任何需要跟系統時鐘同步的工作。 if(Counter<=0) { /* Check for interrupts and do other hardware emulation here */ ... Counter+=InterruptPeriod; if(ExitRequired) break; } 有關週期性的工作,後面會提到。 注意,我們並非直接指定 Counter=InterruptPeriod,而是執行 Counter+=InterruptPeriod,這樣會讓週期的計算更精確,因為有時候,Counter 會變 成負數。 同時,注意這 if(ExitRequired) break; 這個敘述如果在每個迴圈都執行,成本太高,所以只有在中斷髮生時才檢查。這樣就可以 在 ExitRequired=1 時,停止模擬,但又不會花太大的成本。 ------------------------------------------------------------------------- 如何存取被模擬的記憶體? 模擬記憶體存取最簡單的方式,就是把它當成一個攤平的位元組或字元組陣列。如此,存 取記憶體,就是一件微不足道的事情: Data=Memory[Address1]; /* Read from Address1 */ Memory[Address2]=Data; /* Write to Address2 */ 這種簡易的作法,並非永遠可行,原因如下: ‧分頁式的記憶體 記憶體空間,可能被切成小塊,變成可以切換的頁,就是所謂的 banks。例如常見的, 小記憶體位址空間( 64 KB),所使用的擴充記憶體。 ‧映射的記憶體 這塊記憶體空間,可以用數個不同的位址來存取。例如你寫資料到位址 $4000,然後你 在位址$6000,及位址 $8000,你也可以讀到。 ‧ROM 的讀取保護 有些存到卡夾的軟體(例如 MSX 的遊戲),就算你寫到 ROM,回傳成功,事實上 ROM 上的資料也不會改變。這麼做,是為了做軟體保護。為了讓這樣的軟體,可以在你的模擬 器運行,你需要把 ROM 設成唯讀。 ‧記憶體映射到 I/O 系統可能有 I/O 裝置,映射到記憶體位址。存取這樣的記憶體位址,會產生特殊效果, 所以必須被追蹤。 要成功處理上述問題,我們引進幾個函式: Data=ReadMemory(Address1); /* Read from Address1 */ WriteMemory(Address2,Data); /* Write to Address2 */ 所有特殊的處理,包括記憶體分頁,記憶體映射,I/O 的處理,等等,都在函式內處理。 ReadMemory() 跟 WriteMemory() 對模擬器造成很大的 CPU 負擔,因為它們執行的非常 頻繁。因此 這些函式必須寫得越有效率越好。這裡有一個存取分頁式記憶體的例子: static inline byte ReadMemory(register word Address) { return(MemoryPage[Address>>13][Address&0x1FFF]); } static inline void WriteMemory(register word Address,register byte Value) { MemoryPage[Address>>13][Address&0x1FFF]=Value; } 注意那個 inline 關鍵字,它會指示編譯器,直接把這些函式碼,直接插入程式中,以 取代函式呼叫。如果你的編譯器,不支援 inline 或是 _inline,試著改把這些函式,宣 告成 static,有些編譯器(例如 Watcom C)最佳化時,會把短的函式,變成 inline 函 式。 同時要記住,通常 ReadMemory() 的呼叫次數,是 WriteMemory() 的好幾倍。所以儘量 把程式碼放到 WriteMemory(),讓 ReadMemory() 保持簡單。 關於記憶體映射的一個小註記: 之前說過,被映射的記憶體,寫入一個位址,可以在其他位址讀取。這個功能,可以實做 在 ReadMemory(),但是通常我們不這樣做,因為 ReadMemory() 比 WriteMemory() 更頻 繁被呼叫。更有效率的方式,是實做記憶體映射到 WriteMemory()函式。 ------------------------------------------------------------------------- 週期性的運作有哪些? 週期性的運作,是被模擬的機器,固定一段時間,就會執行的工作,例如: ‧螢幕更新 ‧VBlank 跟 HBlank 系統中斷 ‧更新時鐘 ‧更新聲音參數 ‧更新鍵盤跟搖桿狀態 ‧其他 為了要模擬這樣的運作,你要替它們綁上固定的週期。例如 CPU 假設以 2.5 MHz,並且 以 50 Hz 更新顯示(PAL 系統),所以 VBlank 系統中斷,就會每 5000 CPU 週期,發 生一次。 2500000/50 = 50000 CPU cycles 現在,假設整個螢幕是(包含 VBlank)是 256 條掃瞄線,實際上只有 212 條顯示(44 條在 VBlank),我們得到一條掃瞄線 195 個 CPU 週期,更新一次。 50000/256 ~= 195 CPU cyles 然後,我們應該產生一個 VBlank 系統中斷,然後在 VBlank 期間不做任何事情。 (256-212)*50000/256 = 44*50000/256 ~= 8594 CPU cycles 小心計算每個週期性運作所需的 CPU 週期,然後使用他們的最大公約數,作為中斷檢查 的週期,然後綁定給每個週期性運作。 ------------------------------------------------------------------------- 如何最佳化 C 程式碼? 首先,很多執行效率的增進,只要選對編譯器的編譯選項,就有了。根據我的經驗,下面 的編譯選項,可以給你的最佳的執行速度: Watcom C++ -oneatx -zp4 -5r -fp3 GNU C++ -O3 -fomit-frame-pointer Borland C++ 如果你發現,這三個編譯器,更好的最佳化參數,或是其他的編譯器的最佳化參數,請讓 我知道。 ‧一些關於把迴圈攤平的筆記 雖然說,把迴圈攤平的這個最佳化選項,看起來是有用的。這個選項,會把短的迴圈, 攤平成線性的敘述。但我的經驗告訴我,開啟這個選項,執行效率並不會提升太大,反而 在某些情況下,程式反而會出現異常。 最佳化 C 程式碼,比選擇編譯器選項,還難搞。跟執行你的程式的 CPU 有很大關係。有 一些通用的規則,可以適用在所有 CPU。但別把它們當成真理。 ‧使用分析程式 用分析工具來執行你的程式(第一個就想到 GPROF),或許可以發現你從沒懷疑的神奇 事情。你會發現毫不起眼的程式,頻繁的被執行,拖慢整個程式。最佳化這些程式碼,或 是用組合語言改寫,可以讓你的程式執行效率飛耀。 ‧不要用 C++ 不要用任何非用 C++ 不可的架構。C++ 跟純 C 比起來,額外的開銷比較大。 ‧整數的型別 儘量用你的 CPU 支持的整數型別。舉例 int 對比 short 或 long,這會減少編譯器產 生不同整數行別的轉換。 ‧暫存器配置 儘量減少在程式區塊配置太多變數,並且宣告他們為 register (大部分的編譯器已經 會自動把變數變成 register)。特別是有很多通用暫存器的 CPU (PowerPC)這個優勢 ,就比有專屬暫存器(Intel 8086)來的強。 ‧攤平小迴圈 如果你剛好有小迴圈會執行好幾次,把小迴圈攤平成線性執行的程式,是好主意。對照 前面提到的編譯器自動攤平選項。 ‧算術移位 vs. 乘除法 儘量用算術移位,如果你乘或除一個數是 2 的 n 次方(J/128==J>>7),算術移位在大 多數的 CPU 都比較快。另外用位元的 & 來求餘數(J%128==J&0x7F)。 ------------------------------------------------------------------------- 什麼是高低字節順序? 所有的 CPU 通常都根據它們如何儲存資料到記憶體,分為幾個等級。除了非常特殊的種 類,絕大多數的 CPU 分成兩個等級: ‧High-endian CPU 先存放 higher byte of word。例如,在這樣的 CPU 你存放 0x12345678,記憶體的內容會長像這樣: 0 1 2 3 +--+--+--+--+ |12|34|56|78| +--+--+--+--+ ‧Low-endian CPU 先存放 lower byte of word。上述了例子,記憶體內容會看起來完全 不一樣。 0 1 2 3 +--+--+--+--+ |78|56|34|12| +--+--+--+--+ 典型 High-endian 的 CPU 有 6809,摩羅托拉 680x0 系列,PowerPC,及昇陽的 SPARC 。Low-Endian 的 CPU 有 6502,及其後代 65816,及 zilog Z80,絕大多數 Intel CPU (8086,8088),DEC alpha 等。 當我們寫模擬器時,必須注意到,你模擬的 CPU,及執行你的模擬器的 CPU 的高低字節 。舉例,我們想要模擬 low-endian 的 Z80,Z80 會先存 lower byte of word。如果你 用的也是 low-endian 的 CPU,例如 intel 8006,那麼完全不需要特別處理。但是如果 你用的是 high-endian 的 CPU,例如 PowerPC,這時候,要存放 16 bit 的 Z80 資料到 記憶體,就會有問題。如果你的程式,必須兩種高低字節順序的 CPU 都能跑,問題就更 複雜了。 一種解節高低字節順序的作法如下: typedef union { short W; /* Word access */ struct /* Byte access... */ { #ifdef LOW_ENDIAN byte l,h; /* ...in low-endian architecture */ #else byte h,l; /* ...in high-endian architecture */ #endif } B; } word; 可以看到,可以用 w 存取整個字節。而每次如果你需要存取個別 byte,用 B.l 及 B.w ,來對應高低位元組。 如果你的程式,要在跨平台編譯,在程式開始執行前,你也許會想要測試,是否編譯有設 定正確的 endian 旗標。這裡有如何測試的程式碼。 int *T; T=(int *)"\01\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; if(*T==1) printf("This machine is high-endian.\n"); else printf("This machine is low-endian.\n"); ------------------------------------------------------------------------- 如何讓程式具可移植性? 尚未撰寫。 ------------------------------------------------------------------------- 為何我要模組化我的程式? 大多數的電腦系統,是由幾塊比較大的晶片所組成,各自執行一部分的系統功能。有 CPU ,顯示控制器,聲音產生器,及其他。有些晶片,有自己的記憶體,及周邊的硬體。 一個典型的模擬器,應該重現原有的系統設計,並實做每個子系統的功能,在不同的模組 。這樣做,首先除錯會比較容易,因為問題會被獨立在各自的模組裡。其次模組化,可以 讓你在別的模擬器,重複使用你的模組。電腦的硬體,其實標準化成度很高,你可以在不 同型號的電腦,發現相同的 CPU,相同的顯示控制器。為某個晶片,模組化寫一次模擬的 程式,會比你每次都重寫來的容易。 -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 218.166.232.91
singy:SS相關文件 http://koti.kapsi.fi/~antime/sega/docs.html 05/13 21:36
singy:本來想參考相關資料找出SS ROM的文本所在... 05/13 21:39
protect6090:謝singy補充 05/13 21:44
conpo:感謝分享與補充 05/14 19:48