超级任天堂编程/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