跳转到内容

超级任天堂编程/DMA 教程

来自维基教科书,开放世界中的开放书籍

什么是 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 详解

[编辑 | 编辑源代码]

为了理解 DMA 的工作原理,我们将简要了解超级任天堂如何处理内存。

控制台基本上有三个总线,它们与内部的多个设备相连。这三个总线是

  1. 总线 A:一个 24 位宽地址总线,它处理 CPU、卡带(ROM + SRAM)和主内存(WRAM)之间的通信——这是主要的总线。
  2. 总线 B:一个 8 位宽地址总线,可以通过主总线地址空间中的特殊地址(硬件寄存器)使用,它将 APU(音频)和 PPU(视频)与它连接起来。
  3. 一个 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 个字节。

DMA 寄存器

[编辑 | 编辑源代码]

注意: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

[编辑 | 编辑源代码]

这里是一个将数据加载到 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

外部链接

[编辑 | 编辑源代码]
华夏公益教科书