【原创】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)默认第一行不显示。等。
结束。