這一波要說的是第八波貼出來的HelloWorld代碼。
這是不是你見過的最長HelloWorld代碼嗎?如果不是,請給我評論。
說起HelloWorld就要涉及顯示文字,在NES里面,就是驅動PPU的事了。
游戲的幾個要素就是畫面、聲音、手控和內部控制邏輯等。
本篇只談及畫面(的一部分)。
本篇的知識點來自《任天堂產品系統文件》。
關於匯編指令,我只簡單解說,詳看《6502微處理機及其應用》,《學習機6502匯編語言》前三章,《任天堂游戲編程探密》,《電腦游戲機硬件與編程特技》。
以上書籍在我網盤可以見到。
按照順序應該說說文件頭。但文件頭里面目前用得到的很低少,還是不要再說,等到最后說一下,反而好理解。
(1)程序開始運行的地址。
一般人寫程序不用了解程序有多長,程序放什么地址,對於用C#的我,以上說的都沒有。
但是NES是一個面向硬件的編程項目,那就不得不按着硬件的路子走。
我們來說說NES存放程序的空間。NES有16位地址空間,即從$0000到$FFFF,而ROM最多是32Kb,對應地址在$8000到$FFFF。也可以接16K的ROM,對應地址是$C000到$FFFF。
(注:大家聽說過的擴容,沒有擴展地址空間段,而是在原有的固定的地址段更換不同的頁。)
對於只有顯示HelloWorld這么簡單的功能,我只選用16Kb的容量去寫就行,這依然有98%以上的空白。
既然只有16Kb那么程序就是放在$C000到$FFFF這一地址段里面。
所以程序的第一行是:
.ORG $C000
reset:
這是指定程序開始的地址。呀,帶分號起頭的是注釋。我跳過了注釋。
注意ORG前面有一個小號,表明ORG是一條偽指令。
簡單起見也可以從$C000開始運行程序。那么在.ORG $C000的下一行,我加了一個標號reset: 這個標號就是指向了$C000。
事實上只要是在這個程序段里面,從哪兒開始運行都可以。代碼的最后會指定程序開始運行的位置。我們現在假定在$C000開始,那么在程序最后將$C000寫到特定位置,那么就可以了。
(2)開機例行代碼。
(2.1)開始的指令。
SEI CLD
SEI是關閉中斷IRQ,現在是剛開機有好多東西要設置,中斷不要來搗亂。
CLD是關閉十進制運算功能,原版6502是含這個功能,NES用的不是原版6502,它的CPU是訂制的,特意不含“十進制運算”功能,不過要主動關閉它,否會出錯的。
(2.2)清空內存。
用循環的方式實現,用了一個最省代碼的寫法。將$0000到$07FF,都清空了,其中$0100到$01FF是6502的棧道,也可以不清空,因為使用的時候總是先寫入,再讀取。
LDX #$ff ; 初始化棧頂指針到$FF TXS INX ; x=0了 _loop_1: ; 清理全部內存 STA $00,x ; STA $0100,x ; 棧,可以不清理,清理就心理好看一些,上面已置了S,就足夠了。 STA $0200,x STA $0300,x STA $0400,x STA $0500,x STA $0600,x STA $0700,x INX ; X每次加1,當X=$FF,加1就是0。(8位CPU循環加法。) BNE _loop_1 ; 當激發零(Z)標志,BNE條件不滿足,不再跳轉。於是下一行。
為什么S要置成$FF?
因為入棧時,S總是自動+1,再寫入。那么第一寫入,S+1得到0。那就從0開始寫入啦。
這個循環寫得不規范,不過它利用8位機的循環加法原理。
(2.3)PPU熱機。
PPU啟動比CPU慢得多,一般要等2幀的時間才進入正常。
_vb1: ; 1幀 BIT $2002 BPL _vb1 ; 進入vblank,不過$2002的D7不再置1,等到結束vblank,再次進入vblank才會置1 _vb2: ; 2幀 BIT $2002 BPL _vb2
CPU清完內存還不到1幀,所以要先等到1幀的結束,再等到vblank結束,如此2次。
書曰:Vblank 標志:1= PPU 在 Vblank 狀態。當 Vblank 結束或 CPU 讀$2002 時,該標志被復位為 0。
這就是要一個時間,不用太精准的。
(3)關閉屏幕的輸出(黑屏)
因為對PPU的背景區地址更新數據,會令整個背景屏幕都移位。用戶會看這種情況,稱之為閃屏、花屏。我們會在最后恢復屏幕的位移,不過這個過程會有一閃的感覺。對於靜態的屏幕更新,黑屏是常見辦法。黑屏之后,PPU沒有輸出,那么內部數據就影響不到用戶的觀看了。
不過動態情況下,這黑屏又會有閃屏的感覺。動態刷新用到中斷NMI。以后再說。
LDA #$00 ; 關屏 STA $2001 STA $2000
CPU: $2001的D3=0,屏幕使能=0。
CPU: $2000的D2=0,命名表讀寫時地址自動+1。(這兩個是PPU的重要控制地址。)
只有關閉的位發生作用,其它控制位不管了,反正沒有作用。開屏再補充正確的參數。
上一篇的源碼在這兒有一個小bug,漏了一行,不過模擬器默認通通是0,也沒有特別問題。
(4)設置顏色
書曰:NES 有兩個調色板,背景(即命名表)調色板和精靈調色板。調色板不包含實際的 RGB 值,它們更象一 個索引表。寫到$3F00-$3FFF 的 D6-D7 字節被忽略。。。。$3F20-$3FFF 全部都是這兩個調色板分別的映像。
寫到這兒,我特地找了不少資料,關於顏色設備的說明,少得可憐。
見《電腦游戲機硬件與編程特技》P28。
大概情況是這樣的:
(4.1)顏色的值,對照書上的圖片,要么YYCHR。只有低6位有效。即只可取$00到$3F。大於3F就是出現循環了。見《任天堂游戲編程探密》P25。
(NES有2套地址,一套是CPU的,另一套是PPU的。顏色地址、命名表、圖案表都是PPU的地址。程序地址、內存地址、音樂控制地址、手柄地址和PPU控制地址都是CPU的地址。PPU控制地址不是PPU自己的地址,就像家里的門牌是掛在門外的,不在門里面。)
(4.2)背景(即命名表)用顏色的地址范圍:$3F00-$3F0F。共16個地址,從第1個開始順數,1個字節是一個顏色。4個字節為一組,或者說一個調色板。PPU:(3F00-3F03)(3F04-3F07)(3F08-3F0B)(3F0C-3F0F)// 在模擬器VNES里面的命名表/屬性表查看器,可以看見BGPAL,就背景調色板。
(4.3)精靈用顏色的地址范圍:$3F10-$3F1F。同上,一樣是4個字節為一個調色板。PPU:(3F10-3F13)(3F14-3F17)(3F18-3F1B)(3F1C-3F1F)
什么叫調色板,這里指PPU畫面的局部區域只能使用一組顏色。// SPPAL就是精靈調色板。
(4.4)背景和精靈是兩個不同系統,它們只有層疊關系,使用顏色和像素方面是無關的。
(4.5)背景中,每16*16像素的方塊區域必須使用同一組顏色(或者說,一個調色板)。你想像背景是由尺寸為16*16的方塊平鋪的,每個方塊只能有4個色。
(4.6)精靈中,每個精靈單位,只使用同一組顏色(或者說,一個調色板)。即一個精靈除了透明色,只能上3個色。
(4.7)統一底色,我發現背景的調色板第一個色被強制統一。也就是我們寫入3F00,一個值。3F04,3F08,3F0C都會變成這個值。
(4.8)掩碼、透明色。精靈所用的調色板第一個色被認定為透明色。這樣精靈才有邊緣呀。
HelloWorld的設置顏色就最簡單了,不用精靈的調色板,就是背景調色板就只用了一個。那么就只寫一個就可以了。
為什么就是第一個調色板(即0號調色板)?因為我下一步清空命名表,同時也清空對應的屬性表,那就是屬性表每個值都是0。所以對應0號調色板。
見《電腦游戲機硬件與編程特技》P33。
上代碼。
; 第一步指定地址 LDA #$3F ; 寫入配色盤(指向$3F00) STA $2006 LDA #$00 STA $2006 ; 第二步連續寫入數據。前提$2000的D2位=0,令地址自動+1的功能設為有效。 LDA #$0F ;0#=黑色 STA $2007 LDA #$30 ;1#=白色 STA $2007 LDA #$2B ;2#=淺藍色 STA $2007 LDA #$15 ;3#=紅色 STA $2007
先要指定PPU的地址,再寫入數據。
我們打算用背景來顯示HelloWorld,並選用第一個調色板,那么指向背景的顏色地址PPU: $3F00。
怎么定義一個16位的地址呢?我們可以分兩次寫入,第1次寫高位地址,第2次寫低位地址。地址寫入CPU:$2006。
然后就是寫入數據,數據就向CPU:$2007寫入。
因為前面設定了CPU:$2000的D2=0。(其實將整個8位都設成了0),所以PPU寫入數據后,地址自動+1,那么可以連續寫入數據,不用一個個去指定地址。
(5)清空命名表和屬性表
我用了兩重循環,倒計數的循環寫法,這個是正規的。因為字節數達到4*256,超出了8位的能力呀,所以X和Y都用上了,還有A也出力。過程要點與上面顏色設置是一樣的,就不多說了。
LDA #$20 ; 清除背景2000-23FF即0頁背景。 STA $2006 LDA #$00 STA $2006 LDY #$04 _loop_ppu_1: LDX #$00 LDA #$00 _loop_ppu_2: STA $2007 DEX BNE _loop_ppu_2 DEY BNE _loop_ppu_1
見《任天堂游戲編程探密》P18
命名表與屬性表的對應關系。見《電腦游戲機硬件與編程特技》P34。
(6)再等一幀,這個好像沒有必要。。。這個在上面(2.3)說過了,就不多說。
(7)設置PPU的工作方式
LDA #$08 ; (D7=0)禁nmi中斷, ; (D5=0)精靈=8*8,(D6=x) ; (D4=0)圖庫:背景用0頁, ; (D3=1)圖庫:精靈用1頁, ; (D2=0)PPU寫入自動+1, ; (D1D0=00)命名表=2000 STA $2000
$2000的各位功能見《任天堂產品系統文件》書本第8節IO端口。
首先,D7=0,HelloWorld這么簡單用不着NMI,也沒有打算寫NMI代碼,所以禁了它。NMI是一個外部中斷,來自PPU,所以設置PPU不要發信號過來就OK。
接下來,D5=0,我們用不着精靈,設成0或1都沒有影響,所以這個不管,設置0算了。
接下來,D4=0,我打算圖案前面一面就放背景的圖案,后面空了就算了,所以背景用0頁。
接下來,D3=1,精靈用1頁。這個其實也沒有所謂,與背景用同一頁也沒有影響。這只不是默認設置。
接下來,D2-=0,這個重要,地址+1,方便地址連接寫入。如果要豎直刷寫命名表,才會用地址+32的設置。
接下來,D0D1=00,只顯示HelloWorld,隨便用第一命名表就行。用哪個命名表都行,只是對應地址要改改。
(8)設置PPU的顯示方式,隨手開屏幕
LDA #$08 ; (D7D6D5=000)底色=黑 ; (D4=0)不顯示精靈 ; (D3=1)顯示背景(開屏) ; (D2=0)左8列像素不顯示精靈,可以將精靈藏在其中 ; (D1=0)左8列像素不顯示背景,可用來做滾屏 ; (D0=0)顯示模式=彩色 STA $2001
忽然覺得這都好簡單,不用多說了。書上都有寫的。
上面的這些都只要在關屏后,先后次序都不重要,要以調次序。上面的代碼,只要拿掉顏色設置,都可以看成開機標准代碼來看了。
(9)關屏,呀前面才開屏,又關屏。多余了。。。呀我寫出來只是為了代碼的標准化。
關屏,然后填寫屏幕上顯示的圖案,文字等。
(10)定位在第2行,第2列開始。為什么不是第1行第1列?因為就是沒設置掩碼,好多模擬器會默認鎖死第1行和最后一行是掩碼區,不顯示。而第1列和最后1列也是很有可能默認鎖死,不顯示。大家可以改代碼試試。(這里說第1列,指的是chr(或Pattern)單位,就是上面代碼注釋寫的“左8列像素”)
但,怎么知道第2行第2列在哪個地址?我說一行是32(=$20)個字節。
那么
第n行m列就是 $2000+(n-1)*$20+(m-1)
定位屏幕的背景坐標就靠上面這個公式了。好像比高數的矩陣簡單一點點。不過背景一般不是用來定位刷新的。而是整幅清刷的。所以不用太擔心。
見《電腦游戲機硬件與編程特技》P31,有一個表格,可以直觀地看出命名表與背景顯示的關系。
; 確定位置在$2021(即第2行的第2列);注,從$2000開始,每行32個圖塊 LDA #$20 STA $2006 LDA #$21 STA $2006
(11)連續寫入字母的ASCII碼
這么簡單?難道NES也認ASCII碼?非也非也。這是我在圖案表上做了手腳,令圖塊的ID剛好對應ASCII碼。剛好一個字母就用一個CHR。
如果要大字體,要2*2個CHR(或以上)顯示一個字母。就在想別的辦法了。關於CHR的教程,我說得太多。這兒不說了。
我解釋一下,向命名表寫入什么數據,屏幕會有什么顯示。
我們的CHR是8*8像素的小方塊。命名表的每個地址對應屏幕上一個8*8像素的小方塊。
(12)修正屏幕的移位
我們上面說了,凡是寫入命名表都會令背景顯示移位。我們現在沒使用滾屏,那么屏幕的顯示坐標應該是(0,0),我們向CPU: $2005寫這個坐標就OK。先寫入X坐標,再寫入Y坐標。
LDA #$00 ; 復位PPU的顯示位置(對應0頁($2000)背景就是(0,0)) STA $2005 STA $2005
(13)開屏,這個上面(8)也題到過了,不用多說。
(14)沒有程序要運行了,那進入死循環。
end: JMP end
(15)中斷,兩個中斷NMI和IRQ,我們都不用,不過例行要寫個RTI指令,好習慣。
(16)3個重要地址指針
.ORG $fffa
.DW nmi, reset, irq
這個好重要。第一,它的位置,我們定位到CPU:$FFFA。這是6502CPU默認的跳轉讀取位置。
第一是NMI中斷開始運行的位置,占兩個字節。
第二是reset,程序開始運行的位置,本篇開頭(1)就說了,設定好這個開始的位置點。
第三是IRQ中斷開始運行的位置,占兩個字節。
關於中斷,本篇暫時不講。
總結一下:
利用一個字母就是一個CHR的小字體,將字母的ASCII碼與字母圖案在CHR文件中的位置(即ID)一一對應。在命名表上寫入CHR的ID,就會顯示對應的CHR,那么ID與ASCII對應,只要寫入ASCII碼就能顯示小寫體字母。實現HelloWorld。
當然,你要有顏色設置,否則顏色不知對應哪個可能就是底色,那看不見。
還有設置屬性表,對應調色板,否則不知哪個,又會看不見。
還有輸出命名表的位置,如果選第一行,那就看不見,大多數模擬器(例如VNES)默認第一行不顯示。等。
結束。