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

结束。

 

posted on 2021-10-24 00:44  大魔司教教主  阅读(483)  评论(1编辑  收藏  举报

导航