【原創】NES第九波:解說HelloWorld


這一波要說的是第八波貼出來的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)默認第一行不顯示。等。

結束。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM