超級任天堂程式設計/DMA 教程
超級任天堂(除了慢速記憶體訪問時間之外)的主要限制之一是主處理器。正如 記憶體對映頁面 中的文字所述,這主要是由於 CPU 應該向後相容,這意味著超級任天堂最初應該同時執行 NES ROM 和 SNES ROM。為此,CPU 具有所謂的模擬模式。
原始任天堂娛樂系統中的處理器是 Ricoh 2A03(NTSC)或 Ricoh 2A07(PAL),實際上是簡化版的 6502。它缺少 6502 的二進位制編碼十進位制數支援(在 IEEE754 規範釋出之前,曾一度用於浮點數),但在其他方面與該處理器相容。超級任天堂娛樂系統執行在 Ricoh 5A22 上,基本上是 65816 的增強版。這個 CPU 具有前面提到的模擬模式,可以像 NES 一樣執行遊戲。此外,超級任天堂使用與舊 NES 相同的方法來定址超過 CPU 本地暫存器大小(16 位)的記憶體,方法是使用與舊 NES 相同的方法(銀行切換)。請注意,這些方法也可以在模擬模式下使用。2A03 和 5A22 的時鐘速度非常相似
| 控制檯 | NTSC 版本的時鐘速度 | PAL 版本的時鐘速度 | 記憶體 |
|---|---|---|---|
| NES | 1.79 MHz | 1.77 MHz | 2048 位元組(2 KB) |
| SNES | 3.58 MHz | 3.55 MHz | 131072 位元組(128 KB) |
請注意,PAL 版本通常會略微慢一些,因為 NTSC 每秒渲染 60 個(交錯半)幀,而 PAL 僅渲染 50 個。
之所以在這篇文章中更多地介紹 CPU 規格而不是直接訪問記憶體(稍後會介紹),是為了說明超級任天堂與 NES 相比的 CPU 優勢很小。超級任天堂的記憶體是 NES 的 64 倍,但 CPU 功率僅是 NES 的 2.5 倍。
一個可能導致 CPU 速度明顯下降的問題(即使在當今時代)是從裝置 A 到裝置 B 複製資訊(位元組)。CPU 通常比儲存當前狀態的記憶體更快,等待記憶體控制器獲取/設定指定的位元組範圍可能會浪費幾個時鐘週期,在這幾個時鐘週期中,如果 CPU 不是 亂序 超標量 的,就會完全阻止當前程式(即遊戲的 ROM)的執行。不用說,幾個 Ricohs 並不是為了成為那種特定型別的 CPU。
直接記憶體訪問是一種獨立於 CPU 將記憶體動態複製到另一個位置的方法。在任何現代計算機中,DMA 都是一項基本需求。對於超級任天堂來說,DMA 可以用來快速將瓦片和調色盤資料複製到影片 RAM(也稱為 VRAM),否則這些資料將由緩慢的 CPU 複製。瞭解 DMA 是建立大型超級任天堂程式的必要條件,因此本教程將介紹 DMA。
DMA 用於將圖形資料(如 8x8 瓦片和瓦片地圖)複製到 VRAM,並將調色盤資料複製到 CGRAM。這些位置只能透過反覆讀寫某些硬體暫存器來訪問(有關更多資訊,請參閱 記憶體對映 和 硬體暫存器 頁面)。
為了理解 DMA 的工作原理,我們將簡要了解超級任天堂如何處理記憶體。
控制檯基本上有三個匯流排,它們與內部的多個裝置相連。這三個匯流排是
- 匯流排 A:一個 24 位寬地址匯流排,它處理 CPU、卡帶(ROM + SRAM)和主記憶體(WRAM)之間的通訊——這是主要的匯流排。
- 匯流排 B:一個 8 位寬地址匯流排,可以透過主匯流排地址空間中的特殊地址(硬體暫存器)使用,它將 APU(音訊)和 PPU(影片)與它連線起來。
- 一個 8 位寬資料匯流排,它由兩個地址匯流排控制,用於將資料傳送到不同的位置。當您發出一個或多個 DMA 過程時,此匯流排將被 CPU 阻塞。
請注意,資料匯流排實際上只有一位元組寬,而 CPU 則提供各種 16 位暫存器。這意味著一個 16 位暫存器不能在一個匯流排週期時鐘內寫入或讀取,而只能在兩個週期內寫入或讀取。
超級任天堂的 CPU 包含一個 DMA 控制器,它總共支援 8 個 DMA 通道。這意味著可以同時設定和啟動 8 個從一個裝置複製塊到另一個裝置的過程。每個通道都可以專門配置為以特定方式執行。通道執行其程式,而通道 0(索引 0)具有最高優先順序,通道 7(索引 7)具有最低優先順序。
通用 PC 中的 DMA 控制器可以配置為以特定方式執行分配給它的任務,這也稱為“模式”。此外,PC DMA 控制器可以用於各種其他裝置,例如連線到系統匯流排(ISA、PCI、AGP、PCIe)的所有裝置。
超級任天堂只支援一種模式,“突發”,這基本上意味著只要有一個 DMA 過程尚未完成,CPU 就會完全停止。原因是,CPU 和 DMA 控制器都使用系統匯流排來與其他裝置通訊,但一次只能有一個裝置(主裝置)使用系統匯流排——CPU 或控制器(更現代的控制器提供了機會傳輸少量資料,然後將系統匯流排的控制權交還給 CPU,立即再次請求系統匯流排,以便 CPU 可以完成其掛起的操作,然後將匯流排的控制權交還給控制器——或者不交還,如果 CPU 必須執行大量記憶體訪問操作)。此外,超級任天堂控制器只能與 APU 或 PPU 通訊。
控制器的暫存器透過系統銀行 $00 – $3F 和 $80 – $BF 中的 $4200 到 $4400 的硬體暫存器公開。有一個主暫存器,寫入它後,控制器將根據寫入位元組的位掩碼啟用要啟動的所有 DMA 傳輸。後續暫存器將包含有關每個通道如何執行的資訊,以及在啟用時該通道應該做什麼。
雖然普通的 DMA 控制器只能以“突發”模式複製它被程式設計為複製的資料,而且最多複製 64 KB(整個一個銀行),但超級任天堂提供另一種方法來複制塊到不同的裝置,這種方法有點類似於 PC DMA 控制器的執行方式。這種型別的 DMA 稱為 HDMA(水平直接記憶體訪問),只在 H 空閒期間執行(即光束雷射器在繪製完一行後重置到原始位置所用的時間)。由於這個時間跨度非常小,因此在一個 H 空閒期間只能傳輸 1 到 4 個位元組。
注意:x 是通道的索引(由 0 到 7 索引)。因此,基本上,每個通道保留了 16 位元組的地址空間。
| 地址 | 描述 |
|---|---|
| $420B | 主 DMA 暫存器。向它寫入一個位元組時,控制器將根據位元組的位掩碼啟用通道。例如,73(0100 1001)將啟用通道 6、3 和 0。 |
| $43x0 | 指定通道應該如何執行傳輸,同樣使用位掩碼(有關更多資訊,請參見下文)。 |
| $43x1 | DMA 通道 x 目的地($21xx) |
| $43x2 | DMA 通道 x 源地址偏移量(低位) |
| $43x3 | DMA 通道 x 源地址偏移量(高位) |
| $43x4 | DMA 通道 x 源地址銀行 |
| $43x5 | DMA 通道 x 傳輸大小(低位) |
| $43x6 | DMA 通道 x 傳輸大小(高位) |
注意:本教程不完整,尚未經過測試。
注意:DMA 通道的傳輸大小,當設定為 #$0000 時,將被讀取為 65536 位元組的傳輸,而不是 0 位元組。
這裡是一個將調色盤資料載入到 CGRAM(儲存調色盤的地方)的宏。
;macro for loading palette data into the CGRAM
;only use if SIZE is less than 256 bytes
;syntax SetPalette LABEL CGRAM_ADDRESS SIZE
.macro SetPalette
pha
php
rep #$20 ; 16bit A
lda #\3
sta $4305 ; # of bytes to be copied
lda #\1 ; offset of data into 4302, 4303
sta $4302
sep #$20 ; 8bit A
lda #:\1 ; bank address of data in memory(ROM)
sta $4304
lda #\2
sta $2121 ; address of CGRAM to start copying graphics to
stz $4300 ; 0= 1 byte increment (not a word!)
lda #$22
sta $4301 ; destination 21xx this is 2122 (CGRAM Gate)
lda #$01 ; turn on bit 1 (corresponds to channel 0) of DMA channel reg.
sta $420b ; to enable transfer
plp
pla
.endm
這裡是一個將資料載入到 VRAM(儲存瓦片和瓦片地圖的地方)的宏。
;macro for loading graphics data into the VRAM
;only use if SIZE is less than 256 bytes
;syntax LoadVRAM LABEL VRAM_ADDRESS SIZE
.macro LoadVRAM
pha ; save the current accumulator, Y index and status registers for the time the function is executed.
phy
php
rep #$20 ; set the accumulator (A) register into 16 bit mode
sep #$10 ; set the index (X and Y) register into 8 bit mode
ldy #$80 ; we will try to write 128 ($80) bytes in one row ...
sty $2115 ; ... and we will let the PPU let this know.
lda #\2 ; the controller will get the hardware register ($2118) as location to where to write the data.
sta $2116 ; but we still need to specify WHERE in VRAM we want to write the data - what we are doing right now.
lda #\3 ; number of bytes to be sent from the controller.
sta $4305
sep #$20 ; set the accumulator (A) register into 8 bit mode
lda #\1 ; from where the data is supposed to be loaded from
sta $4302
ldy #:\1 ; from which bank the data is supposed to be loaded from
sty $4304
ldy #$01 ; set the mode on how the channel is supposed to do it's work. 1= word increment
sty $4300
ldy #$18 ; remember that I wrote "the controller will get the hardware register"? This is it. 2118 is the VRAM gate.
sty $4301
ldy #$01 ; turn on bit 1 (channel 0) of DMA - that is, start rollin'
sty $420b
plp ; Restore the state of all registers before leaving the function.
ply
pla
.endm