首发公众号:Rand_cs
总述
本来想直接开始写 CPU 的,想了想,还是先来写个总述,好有个大致概念。
省去了很多东西,但总体大概就这么个样子。手柄是输入设备,电视为输出设备,CPU 为处理器,PPU 为图形处理器,卡带可以看作是存储的一部分
任天堂给出的与 CPU 相关的总线图如下所示:
可以看出地址总线为 16 位,但是数据总线只有 8 位。
按照上图所示,其中 CPU 的地址空间分为三部分,一部分为 ROM,它位于卡带里面,RAM 为 CPU 自个儿的内存,I/O 为内存映射的一些寄存器,CPU 通过这些寄存器来与 PPU,手柄等通讯。
任天堂给出的那张图对于 CPU PPU 地址空间的描述不是很清楚,自个儿画了张图,如上所示,先简要解释出现的名词:
CHR:Character Memory,存放游戏使用的图案
PGR:Program Memory,存放游戏代码
Nametable:VRAM,这部分是 PPU 自个儿的内存,存放图案的索引还有一定的颜色信息。
Pallete:调色板,渲染时使用的颜色(存放的是颜色的索引)
CPU RAM:CPU 自个儿的内存
CPU 的地址空间主要由三部分组成,从低到高依次为:CPU RAM -> 内存映射寄存器 -> PRG
PPU 的地址空间也由三部分组成,从低到高依次为:Nametable->CHR->Pallete
内存映射的寄存器是 CPU 用来和其他子系统通讯使用,比如说与 PPU 通讯,获取 PPU 地址空间里面的数据,与输入设备手柄通讯,与内部的 APU 通讯等等。
另外要注意,虽然 PRG 和 CHR 两者在卡带里面,但是映射到了不同的地址空间。
下面简要的说说这几个部分:
先从卡带说起,卡带里面有主要有两块存储芯片,第一个是 PRG,Program Memory,是只读的 ROM,看名字大概就能够猜到,这里面存放的是游戏代码,这个游戏代码交由 CPU 来运行。
第二个是 CHR,Character Memory,ROM 和 RAM 都有可能,不同的卡带里面不同。这里面存放的是游戏所用到的图案。比如说咱们熟悉的超级马里奥的 CHR:
这是我用 FCEUX 打开超级马里奥之后使用其 PPU Viewer 工具截取的图,有兴趣的可以下载这个模拟器试试。
可以看出超级马里奥的 CHR 里面主要有两个 PatternTable (图案表),这两个 PatternTable 没有明确的名称,但是在超级马里奥里面可以看出,左侧的 PatternTable 主要是精灵使用(精灵就是角色血条分数等等),右侧主要是背景使用。
每个 PatternTable 由 256 个“小块” 组成,这么一个 “小块” 叫做 tile (中文翻译过来叫瓦片?感觉不太好,后面我直接使用英文 tile),一个 tile 就是 $8 \times 8$ 的像素集合。
要表示一个像素图案,1 个像素可以用 1bit 来表示,比如说 0 表示透明,1 表示不透明,那么 $8 \times 8=64bit$ 就可以表示一个图案,但 tile 用 2bit 来表示一个 像素,举个例子:
这大致能看出来是是什么吧,这个 tile 是长大的马里奥头部的右上角,所以 tile 本质上是什么,一个 tile 就是 64 个 2bit 信息,但是要注意一个像素的 2bit 是分开存储的,如上图上侧所示。
为什么要用 2bit 来表示一个像素,这是因为这 2bit 可以在 4 种颜色当中索引(其实只有 3 种,因为有个透明"色"),所以可以知道一个 tile 种绝对不会超过 4 种颜色。还记得我前文说的“抠门”吗?这算是 “抠门” 之一吧。
因此对于 PatternTable,虽然叫做 PatternTable,但它存储的不只是图案信息,还有颜色信息。
大家都知道,所谓的视频、连续的画面都是一帧一帧的图片组成,而 NES 里面,显示到电视的每一帧图片都是由一个个 tile 组成。
那一帧的图片由多少个 tile 组成呢?也就是说图片的像素多少?每一帧图片是 $256 \times 240$ 像素,也就是由 $32 \times 30 = 960$ 个 tile 组成。
960 个 tile 一定是按照某种次序排列的,才能形成正确的图片,那么在哪记录这个次序呢?这就是 PPU 自个儿的 RAM,也就是显存 VRAM,又叫做 Nametable。
一屏或者说一帧图片有 960 个 tile,但记录在 Nametable 中的信息并不是 tile 本身,而是记录 tile 的索引。举个例子:
如上图所示,M 使用索引为 \$16 的 tile,金币使用的是索引为 \$2E 的 tile,草垛和云的一角都使用的是索引为 \$36 的图案,\$ 表示十六进制,在 NES CPU 的汇编里面使用 \$ 表示十六进制而不是 0x。
某个时刻背景只会从某个 PatternTable 中选取 tile,从上面的所举的马里奥例子中来看,此时背景选用的 PatternTable 为右侧的PatternTable。
另外还可以观察到一个事实:云朵和草垛其实是一个东西,只是颜色不一样而已,由此也可以想到:看起来画面丰富多彩,其实很多都是一样的东西,一帧画面再丰富多彩,都不可能超过 $256 \times 2=512$ 个 tile 样式。这算是“抠门”之二吧
所以现在知道了 PPU 的 Nametable 主要就是拿来存放这么一个个的 tile 索引,这里我们可以简单的计算一下:
一个 PatternTable 256 个 tile,所以索引需要 8 bits,一屏有 960 个 tiles 组成,即需要 960B 存放一屏的索引,Nametable 有两个,每个 1KB = 1024B,所以 2 个 Nametable 可以放下 2 屏的 tile 索引。
物理上有 2 屏,但是逻辑上抽象出来 4 屏,其中 2 屏是镜像,这是后话了,这里需要知道的是,有了这更大的空间存放索引,使得滚屏更容易实现(emm也不容易),举个横向滚屏的例子:如下图所示:
上图的上侧就是两屏的索引,只是以图案代替了,那个方框就是屏幕,方框里面的东西就是会渲染到屏幕上的,如下侧所示。
所以现在我们知道了 Nametable 里面就是存放了两屏的 tile 索引,PPU 将会根据这两屏的索引渲染背景。
那这个 Nametable 是被谁填充的呢?这个就是由游戏代码控制的了,什么时候填充,填充什么,都是由代码来进行逻辑控制。
回到前面那简单的计算,一个 Nametable 1024B,但是一屏的 tile 索引只用了 960B,还有 64B 呢?丢了不要了?这不可能,那个年代,寸土寸金,没有浪费这一说。
这 64B 叫做 Atrribute Table,其实就是背景的颜色属性。什么?64B 可以记录一屏的颜色属性?当然不可能,但也差不多了。
说到这里就顺便说说颜色,颜色的表示有多种方式,比如最熟悉的 RGB,一个三元组(R, G, B)就能表示一种颜色,每个元素的取指范围为[0, 255]。
在 NES 里面,理论上能使用的颜色有 64 种,还记得 前面地址空间结构图吗,其中 PPU 里面有一块空间专门来存放颜色信息,但存放的并不是颜色本身,而是 NES 调色板里颜色的索引。
PPU Pallete 里有 8 个条目,每个条目有 4 种颜色的索引。背景只能使用前 4 个,而精灵只能使用后 4 个,感觉挺抠是吧,这还没完,背景使用的 Pallete 都应有相同的一个背景色,所以背景其实只能使用 $3 \times 4 + 1 = 13$ 种颜色,而精灵需要有透明色,所以精灵实际只有 $3 \times4 = 12$ 种颜色可用。
回到 Attribute,Attribute 就是用来选取哪个 Pallete 条目,但 64B 的 Attribute 要为 $32 \times 30$ 个 tile,$256 \times 240$ 的像素选取 Pallete,那必然会使得一些 tile 使用相同的 Pallete,这也是为什么这些游戏的颜色如此单调的原因,这也算是 “抠门” 之三。具体颜色如何选取抉择,还是有些复杂,三言两语说不清,留待后面详述。
前面 PatternTable,这里的 AttributeTable,Pallete,都在说颜色,颜色到底怎么回事?来捋一捋:AtrributeTable 用来选取 Pallete 条目,PatternTable 用来选取 Pallete 中的颜色索引,这个索引又指向实际的颜色。
前面都在说背景,这里再来说说精灵,不知大家有没有注意到,上述的一些图片少了些什么不?少了角色等精灵,精灵与背景是分开单独控制的。
PPU 的内部有专门的 RAM(地址空间中没显示,不能直接访问,需要通过特定的端口来访问) 来存放精灵的信息,这部分空间叫做 OAM(Object Attribute Memory),也叫做 Sprite RAM。OAM 中能存放 64 个精灵条目,但是每次最多只能渲染 8 个精灵。
每个精灵条目控制着精灵的一些属性,比如说这个精灵使用的哪个 tile,也就是 tile 索引,还有精灵的位置,即 X,Y 坐标,另外就是该精灵的使用的 Pallete 条目,是否翻转等信息。
举个例子简单说明,比如说长大的马里奥:
明显的,这长大的马里奥由 8 个 tile 组成,具体是 PatternTable 里面哪个 tile 我就不去标注了。
仔细看这个马里奥的话,还可以发现他的下半部分是对称的,在PatternTable 里面是没有我用红框圈出来的 tile 的,PatternTable 里面只有左侧其对称的图案,这就是“抠门”之四,相同的 tile 直接翻转利用,这里是水平翻转,同样的还有垂直翻转,这里就不举例了。
在某一帧的画面中,一屏的背景是 960 个 tile 索引有序的排列好,这个“有序”由游戏代码逻辑决定,游戏代码规定此时云在天上,那就定死了它在天上不能动,除非更改代码。
但是精灵有些不同,OAM 中的精灵条目有属性项专门控制精灵的位置(X, Y 坐标),理论上精灵一帧中精灵可以在任何位置,不过一个游戏有一个游戏的逻辑,比如说马里奥本身在地上走跑跳,不可能在天上飞是吧。
一般角色的位置是可以由 Controller,比如说手柄来控制的,大致的过程就是手柄按键向 CPU 发送信号,然后监测相应的按键更改 OAM 中的精灵的位置属性,之后 PPU 就会渲染到相应的位置。
背景要渲染,精灵要渲染,它两的像素肯定是会重叠的,PPU 自有逻辑控制和选择哪个的像素输出,这留待后面慢慢说到。
关于这,有意思的一点是:如果第 0 个精灵的不透明像素与背景不透明的像素重叠,那么就会引起 sprite 0 hit,可以利用这个特点来 split creen (屏幕分割?),大致意思就是屏幕上只有一部分滚屏渲染。
emmm 感觉表达的不太准确,举例子说明:在玩超级马里奥的时候会发现顶部的分数,时间等信息是没有随着滚屏而跑出屏幕之外,而是相对静止在屏幕顶部:
这就是依靠 sprite 0 hit 做出来的效果。
另外还可以做出大片级的效果,最为津津乐道的就是忍者龙剑传的过长动画。
这妥妥的大片级效果啊!!!
至于渲染到屏幕上,渲染的顺序与我们看书写字的顺序一样,从上到下,从左到右,如下所示:
这个 PPU 输出的信号需要由 CRT 电视接收,俗称大屁股电视,就是屏幕后面嘿大一坨的那种电视。这种电视的大致显色原理为电子枪发射电子轰击带有荧光粉的荧光屏,荧光粉收到高速电子的激发而发光。
这里有几个术语需要知道,渲染的一行叫做 scanline,每一帧中有 240 条 scanline 可见,刚好对应着我们前面所说的 $256 \times 240$ 的像素。
渲染的每一帧之间都会有一段空隙,这是因为渲染到最后一行后电子枪需要回到左上角,这部分时间就叫做 vblank(垂直消隐)。这部分其实是触发了一个 NMI 中断,CPU 可以在这段时间内更新 NameTable、OAM 等信息用于下次渲染。
这里强烈建议看看这个视频:
【中字】慢镜头下的电视工作原理科普(CRT/LED/OLED)_哔哩哔哩_bilibili
那所谓的渲染是怎么一个过程呢?
对于背景来说,在渲染某一屏背景之前,这一屏背景的 tile 索引一般来说是在 nametable 中已经存放好了的,根据 tile 索引去获取存放在 tile 里面的颜色信息和 AttributeTable 里面的颜色信息,两者组合起来就是实际颜色的索引。
而精灵呢?精灵的 tile 索引和 Attribute 都存放在 OAM(正渲染的精灵条目实际存放在一个缓冲区),同样的根据 Attribute 和 tile 两者中的颜色信息组合成实际颜色的索引
然后 PPU 根据一些状态信息抉择是输出背景的颜色还是精灵的颜色,这个颜色信号发送给电视,电视输出到屏幕。
渲染是最为复杂的过程之一,寥寥几句肯定不够,这后面再详述吧。
最后再来简要介绍前面途中出现过的一个东西,Mapper,也叫 MMC(Memory Management Chip),它的主要作用就是解决游戏大小的限制,CPU 和 PPU 的地址空间有限,如果游戏很大那么就有可能映射不下,所以需要 Mapper 来控制当前地址空间映射到卡带的哪一块空间,术语叫做 bank switching,就是将卡带里面的空间分成一个一个的 bank,然后 Mapper 控制 bank 映射到相应的地址空间。
好了,本文大致就说这么多,主要是想让大家先有个大致的概念,其实我写到后面已经感觉到弄巧成拙了,这些东西三言两语是说不清的,但我又想说清,可能有些地方表述的不太清楚,那就待我后面慢慢说到吧。
本文的话,主要对开头几张图有个印象,CPU 和 PPU 都有自己的总线,地址空间都主要由三部分构成,卡带里面有游戏的代码和游戏使用的图案,但是这两者分别映射到了 CPU 和 PPU 的地址空间。
背景使用的 tile 索引存放在 NameTable,其中包含了 Attribute Table,精灵使用的的 tile 索引和 Attribute 都放在 OAM 中。tile 本身有 2bit 的颜色信息,Attribute 中又有两位的颜色信息,组合起来就是实际颜色在调色板中的索引。
渲染的话就是 PPU 要取得 $256 \times 240$ 个像素点的颜色信息然后发送给 TV,渲染顺序为从上到下从左到右,每一帧的渲染之间都会有叫做 vblank 的空闲时间,这部分时间之内 CPU 就可以去更新 NameTable、OAM 中的一些信息用于下次渲染。
大概就先这么多吧,一些领域我也初次接触,关于 NES 中文基本没有资料,都是直接啃的英文资料,某些地方理解可能有些偏差,一些英文术语没怎么看到也懒得去找官方翻译,我也就直接使用英文了。有什么问题还请批评指正,也欢迎来同我交流,下一篇应该是真的讲述 CPU 6502 了。
首发公众号:Rand_cs