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生命週期如此長的關係吧, 因為什麼都有可能呢..