zoukankan      html  css  js  c++  java
  • 【原创】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)默认第一行不显示。等。

    结束。

  • 相关阅读:
    实验二、作业调度模拟实验
    实验一
    0909 初识操作系统
    实验四、主存空间的分配和回收模拟
    12.27评论5位同学试验三
    实验三进程调度模拟程序
    实验二、作业调度模拟实验
    实验一报告
    实验四 主存空间的分配和回收模拟
    实验三 进程调度模拟程序
  • 原文地址:https://www.cnblogs.com/fogota/p/15415565.html
Copyright © 2011-2022 走看看